// Edit decision lists.
//
// An EDL is read either as JSON:
//
//	{"version":1,"ops":[{"op":"cut","start":"0:12.5","end":"0:20"}]}
//
// or in the compact command form, one operation per line:
//
//	trim  --start 0:02 --end 1:32
//	cut   12.5-20
//	speed 20-40 x4
//	hold  45 --for 2
//	gap   --max 1.5
//
// Both forms produce the same []edlOp. Every time value is parsed exactly into
// a big.Rat; no float64 is involved anywhere in this file.
package main

import (
	"encoding/json"
	"fmt"
	"math/big"
	"strings"
)

const (
	opCut   = "cut"
	opTrim  = "trim"
	opSpeed = "speed"
	opHold  = "hold"
	opGap   = "gap"
)

type edlOp struct {
	Kind   string
	Start  *big.Rat // cut, speed, trim
	End    *big.Rat // cut, speed, trim
	At     *big.Rat // hold
	Dur    *big.Rat // hold --for
	Max    *big.Rat // gap --max
	Factor *big.Rat // speed
	Source string   // the text this op was parsed from, for error messages
	Line   int
}

func (o edlOp) where() string {
	if o.Line > 0 {
		return fmt.Sprintf("line %d: %s", o.Line, o.Source)
	}
	return o.Source
}

type edl struct {
	Version int
	Ops     []edlOp
}

// ---------------------------------------------------------------------------
// Time and factor parsing
// ---------------------------------------------------------------------------

// parseTime accepts 12, 12.5, 12.5s, 500ms, 250us, 2m, 1:30, 1:30.250 and
// 0:01:05. The result is exact.
func parseTime(s string) (*big.Rat, error) {
	t := strings.TrimSpace(s)
	if t == "" {
		return nil, fmt.Errorf("empty time value")
	}
	neg := false
	switch {
	case strings.HasPrefix(t, "+"):
		t = t[1:]
	case strings.HasPrefix(t, "-"):
		neg, t = true, t[1:]
	}
	var r *big.Rat
	if strings.Contains(t, ":") {
		parts := strings.Split(t, ":")
		if len(parts) > 3 {
			return nil, fmt.Errorf("cannot parse time %q: too many colon-separated fields", s)
		}
		r = new(big.Rat)
		for _, p := range parts {
			p = strings.TrimSpace(p)
			v, ok := new(big.Rat).SetString(p)
			if !ok || p == "" {
				return nil, fmt.Errorf("cannot parse time %q", s)
			}
			r.Mul(r, ratMinute)
			r.Add(r, v)
		}
	} else {
		mult := ratOne
		low := strings.ToLower(t)
		switch {
		case strings.HasSuffix(low, "ms"):
			mult, t = ratMillis, t[:len(t)-2]
		case strings.HasSuffix(low, "us"):
			mult, t = ratMicro, t[:len(t)-2]
		case strings.HasSuffix(low, "m"):
			mult, t = ratMinute, t[:len(t)-1]
		case strings.HasSuffix(low, "s"):
			t = t[:len(t)-1]
		}
		t = strings.TrimSpace(t)
		v, ok := new(big.Rat).SetString(t)
		if !ok || t == "" {
			return nil, fmt.Errorf("cannot parse time %q", s)
		}
		r = new(big.Rat).Mul(v, mult)
	}
	if neg {
		r.Neg(r)
	}
	return r, nil
}

// parseFactor accepts 4, x4, ×4, 4x, *4, 1.5 and 3/2. The result is exact, so
// a ×3 speed-up is one third exactly, not 0.3333333333333333.
func parseFactor(s string) (*big.Rat, error) {
	t := strings.TrimSpace(s)
	t = strings.TrimPrefix(t, "×")
	t = strings.TrimPrefix(t, "*")
	if len(t) > 0 && (t[0] == 'x' || t[0] == 'X') {
		t = t[1:]
	}
	if len(t) > 0 && (t[len(t)-1] == 'x' || t[len(t)-1] == 'X') {
		t = t[:len(t)-1]
	}
	t = strings.TrimSpace(t)
	v, ok := new(big.Rat).SetString(t)
	if !ok || t == "" {
		return nil, fmt.Errorf("cannot parse speed factor %q", s)
	}
	if v.Sign() <= 0 {
		return nil, fmt.Errorf("speed factor %q must be greater than zero", s)
	}
	return v, nil
}

// splitRange splits "A-B" or "A..B" into its two halves. A leading minus on the
// first field is honoured, so the split never happens at index 0.
func splitRange(s string) (string, string, bool) {
	if i := strings.Index(s, ".."); i > 0 {
		return s[:i], s[i+2:], true
	}
	for i := 1; i < len(s); i++ {
		if s[i] == '-' {
			// "1e-3" style exponents: do not split there.
			if c := s[i-1]; c == 'e' || c == 'E' {
				continue
			}
			return s[:i], s[i+1:], true
		}
	}
	return "", "", false
}

// ---------------------------------------------------------------------------
// JSON form
// ---------------------------------------------------------------------------

type jsonEDL struct {
	Version int         `json:"version"`
	Ops     []jsonEDLOp `json:"ops"`
}

type jsonEDLOp struct {
	Op     string          `json:"op"`
	Start  json.RawMessage `json:"start,omitempty"`
	End    json.RawMessage `json:"end,omitempty"`
	At     json.RawMessage `json:"at,omitempty"`
	For    json.RawMessage `json:"for,omitempty"`
	Max    json.RawMessage `json:"max,omitempty"`
	Factor json.RawMessage `json:"factor,omitempty"`
}

// rawTime unwraps a JSON value that may be a string ("1:30") or a number (90).
func rawTime(field string, raw json.RawMessage) (*big.Rat, error) {
	if len(raw) == 0 {
		return nil, nil
	}
	s := strings.TrimSpace(string(raw))
	if s == "null" {
		return nil, nil
	}
	if strings.HasPrefix(s, "\"") {
		var str string
		if err := json.Unmarshal(raw, &str); err != nil {
			return nil, fmt.Errorf("%s: %v", field, err)
		}
		s = str
	}
	r, err := parseTime(s)
	if err != nil {
		return nil, fmt.Errorf("%s: %v", field, err)
	}
	return r, nil
}

func rawFactor(raw json.RawMessage) (*big.Rat, error) {
	if len(raw) == 0 {
		return nil, nil
	}
	s := strings.TrimSpace(string(raw))
	if s == "null" {
		return nil, nil
	}
	if strings.HasPrefix(s, "\"") {
		var str string
		if err := json.Unmarshal(raw, &str); err != nil {
			return nil, fmt.Errorf("factor: %v", err)
		}
		s = str
	}
	return parseFactor(s)
}

func parseEDLJSON(data []byte) (*edl, error) {
	trimmed := strings.TrimSpace(string(data))
	var doc jsonEDL
	if strings.HasPrefix(trimmed, "[") {
		if err := json.Unmarshal(data, &doc.Ops); err != nil {
			return nil, fmt.Errorf("malformed EDL JSON: %v", err)
		}
		doc.Version = 1
	} else {
		if err := json.Unmarshal(data, &doc); err != nil {
			return nil, fmt.Errorf("malformed EDL JSON: %v", err)
		}
		if doc.Version == 0 {
			doc.Version = 1
		}
	}
	if doc.Version != 1 {
		return nil, fmt.Errorf("unsupported EDL version %d (this build understands version 1)", doc.Version)
	}
	e := &edl{Version: doc.Version}
	for i, jo := range doc.Ops {
		op := edlOp{Kind: strings.ToLower(strings.TrimSpace(jo.Op)), Line: i + 1}
		op.Source = fmt.Sprintf("ops[%d] %q", i, jo.Op)
		var err error
		if op.Start, err = rawTime("start", jo.Start); err != nil {
			return nil, fmt.Errorf("%s: %v", op.Source, err)
		}
		if op.End, err = rawTime("end", jo.End); err != nil {
			return nil, fmt.Errorf("%s: %v", op.Source, err)
		}
		if op.At, err = rawTime("at", jo.At); err != nil {
			return nil, fmt.Errorf("%s: %v", op.Source, err)
		}
		if op.Dur, err = rawTime("for", jo.For); err != nil {
			return nil, fmt.Errorf("%s: %v", op.Source, err)
		}
		if op.Max, err = rawTime("max", jo.Max); err != nil {
			return nil, fmt.Errorf("%s: %v", op.Source, err)
		}
		if op.Factor, err = rawFactor(jo.Factor); err != nil {
			return nil, fmt.Errorf("%s: %v", op.Source, err)
		}
		e.Ops = append(e.Ops, op)
	}
	return e, nil
}

// ---------------------------------------------------------------------------
// Compact command form
// ---------------------------------------------------------------------------

// parseEDLLine parses one compact-form operation.
func parseEDLLine(line string, lineNo int) (edlOp, error) {
	fields := strings.Fields(line)
	op := edlOp{Source: strings.TrimSpace(line), Line: lineNo}
	if len(fields) == 0 {
		return op, fmt.Errorf("empty operation")
	}
	op.Kind = strings.ToLower(fields[0])
	rest := fields[1:]

	// Named flags first; whatever is left over is positional.
	var positional []string
	for i := 0; i < len(rest); i++ {
		a := rest[i]
		if !strings.HasPrefix(a, "-") {
			positional = append(positional, a)
			continue
		}
		name := strings.TrimLeft(a, "-")
		val := ""
		if j := strings.Index(name, "="); j >= 0 {
			name, val = name[:j], name[j+1:]
		} else {
			if i+1 >= len(rest) {
				return op, fmt.Errorf("flag --%s needs a value", name)
			}
			i++
			val = rest[i]
		}
		var err error
		switch strings.ToLower(name) {
		case "start", "from":
			op.Start, err = parseTime(val)
		case "end", "to":
			op.End, err = parseTime(val)
		case "at":
			op.At, err = parseTime(val)
		case "for", "dur", "duration":
			op.Dur, err = parseTime(val)
		case "max":
			op.Max, err = parseTime(val)
		case "factor", "speed", "by":
			op.Factor, err = parseFactor(val)
		default:
			return op, fmt.Errorf("unknown flag --%s", name)
		}
		if err != nil {
			return op, err
		}
	}

	switch op.Kind {
	case opCut, opTrim, opSpeed:
		for _, p := range positional {
			if a, b, ok := splitRange(p); ok && op.Start == nil && op.End == nil {
				s, err := parseTime(a)
				if err != nil {
					return op, err
				}
				e, err := parseTime(b)
				if err != nil {
					return op, err
				}
				op.Start, op.End = s, e
				continue
			}
			if op.Kind == opSpeed && op.Factor == nil {
				f, err := parseFactor(p)
				if err != nil {
					return op, err
				}
				op.Factor = f
				continue
			}
			return op, fmt.Errorf("unexpected argument %q", p)
		}
	case opHold:
		for _, p := range positional {
			if op.At == nil {
				v, err := parseTime(p)
				if err != nil {
					return op, err
				}
				op.At = v
				continue
			}
			if op.Dur == nil {
				v, err := parseTime(p)
				if err != nil {
					return op, err
				}
				op.Dur = v
				continue
			}
			return op, fmt.Errorf("unexpected argument %q", p)
		}
	case opGap:
		for _, p := range positional {
			if op.Max == nil {
				v, err := parseTime(p)
				if err != nil {
					return op, err
				}
				op.Max = v
				continue
			}
			return op, fmt.Errorf("unexpected argument %q", p)
		}
	default:
		return op, fmt.Errorf("unknown operation %q (want cut, trim, speed, hold or gap)", op.Kind)
	}
	return op, nil
}

func parseEDLText(text string) (*edl, error) {
	e := &edl{Version: 1}
	for i, raw := range strings.Split(text, "\n") {
		line := strings.TrimSpace(raw)
		if j := strings.Index(line, "#"); j >= 0 {
			line = strings.TrimSpace(line[:j])
		}
		if line == "" {
			continue
		}
		op, err := parseEDLLine(line, i+1)
		if err != nil {
			return nil, fmt.Errorf("EDL line %d: %v (in %q)", i+1, err, strings.TrimSpace(raw))
		}
		e.Ops = append(e.Ops, op)
	}
	return e, nil
}

// parseEDL auto-detects the JSON form from its first non-space character.
func parseEDL(data []byte) (*edl, error) {
	t := strings.TrimSpace(string(data))
	if t == "" {
		return &edl{Version: 1}, nil
	}
	if t[0] == '{' || t[0] == '[' {
		return parseEDLJSON(data)
	}
	return parseEDLText(t)
}

// ---------------------------------------------------------------------------
// Validation
// ---------------------------------------------------------------------------

// validate checks an EDL on its own terms, before it ever meets a cast:
// required fields present, ranges well formed, ranges in ascending order and
// non-overlapping, at most one trim and one gap.
func (e *edl) validate() error {
	var trimSeen, gapSeen *edlOp
	var ranges []edlOp // cut + speed, in the order given

	for i := range e.Ops {
		op := &e.Ops[i]
		switch op.Kind {
		case opTrim:
			if trimSeen != nil {
				return fmt.Errorf("EDL %s: a second trim is not allowed (the first was %s)", op.where(), trimSeen.where())
			}
			if op.Start == nil {
				op.Start = rZero()
			}
			if op.End != nil && op.Start.Cmp(op.End) >= 0 {
				return fmt.Errorf("EDL %s: trim start %s is not before end %s", op.where(), rString(op.Start), rString(op.End))
			}
			if op.Start.Sign() < 0 {
				return fmt.Errorf("EDL %s: trim start %s is negative", op.where(), rString(op.Start))
			}
			trimSeen = op
		case opGap:
			if gapSeen != nil {
				return fmt.Errorf("EDL %s: a second gap is not allowed (the first was %s)", op.where(), gapSeen.where())
			}
			if op.Max == nil {
				return fmt.Errorf("EDL %s: gap needs --max <duration>", op.where())
			}
			if op.Max.Sign() < 0 {
				return fmt.Errorf("EDL %s: gap --max %s is negative", op.where(), rString(op.Max))
			}
			gapSeen = op
		case opCut, opSpeed:
			if op.Start == nil || op.End == nil {
				return fmt.Errorf("EDL %s: %s needs a start and an end", op.where(), op.Kind)
			}
			if op.Start.Sign() < 0 {
				return fmt.Errorf("EDL %s: %s start %s is negative", op.where(), op.Kind, rString(op.Start))
			}
			if op.Start.Cmp(op.End) >= 0 {
				return fmt.Errorf("EDL %s: %s range is out of order: start %s is not before end %s",
					op.where(), op.Kind, rString(op.Start), rString(op.End))
			}
			if op.Kind == opSpeed {
				if op.Factor == nil {
					return fmt.Errorf("EDL %s: speed needs a factor, e.g. speed %s-%s x4",
						op.where(), rString(op.Start), rString(op.End))
				}
				if op.Factor.Sign() <= 0 {
					return fmt.Errorf("EDL %s: speed factor must be greater than zero", op.where())
				}
			}
			ranges = append(ranges, *op)
		case opHold:
			if op.At == nil {
				return fmt.Errorf("EDL %s: hold needs a time, e.g. hold 45 --for 2", op.where())
			}
			if op.Dur == nil {
				return fmt.Errorf("EDL %s: hold needs --for <duration>", op.where())
			}
			if op.At.Sign() < 0 {
				return fmt.Errorf("EDL %s: hold at %s is negative", op.where(), rString(op.At))
			}
			if op.Dur.Sign() <= 0 {
				return fmt.Errorf("EDL %s: hold --for %s must be greater than zero", op.where(), rString(op.Dur))
			}
		default:
			return fmt.Errorf("EDL %s: unknown operation %q", op.where(), op.Kind)
		}
	}

	// Ranges must be listed in ascending source order and must not overlap.
	for i := 1; i < len(ranges); i++ {
		prev, cur := ranges[i-1], ranges[i]
		if cur.Start.Cmp(prev.Start) < 0 {
			return fmt.Errorf("EDL %s: %s %s-%s is out of order: it starts before the preceding %s %s-%s",
				cur.where(), cur.Kind, rString(cur.Start), rString(cur.End),
				prev.Kind, rString(prev.Start), rString(prev.End))
		}
		if cur.Start.Cmp(prev.End) < 0 {
			return fmt.Errorf("EDL %s: %s %s-%s overlaps the preceding %s %s-%s",
				cur.where(), cur.Kind, rString(cur.Start), rString(cur.End),
				prev.Kind, rString(prev.Start), rString(prev.End))
		}
	}

	// A hold must not land strictly inside a cut: the frame it would freeze on
	// no longer exists.
	for _, op := range e.Ops {
		if op.Kind != opHold {
			continue
		}
		for _, r := range ranges {
			if r.Kind != opCut {
				continue
			}
			if op.At.Cmp(r.Start) >= 0 && op.At.Cmp(r.End) < 0 {
				return fmt.Errorf("EDL %s: hold at %s falls inside cut %s-%s, so there is no frame left to freeze",
					op.where(), rString(op.At), rString(r.Start), rString(r.End))
			}
		}
	}
	return nil
}
