const EPSILON = 0.001;

function cloneSegments(segments) {
  return segments.map((segment) => ({ ...segment }));
}

export class EditorModel {
  constructor(duration) {
    if (!Number.isFinite(duration) || duration <= 0) throw new TypeError('duration must be positive');
    this.duration = duration;
    this.segments = [{ start: 0, end: duration, name: '', fadeIn: 0, fadeOut: 0 }];
    this.selected = 0;
    this.playhead = 0;
    this.undoStack = [];
    this.redoStack = [];
  }

  snapshot(action = 'edit') {
    return { action, selected: this.selected, playhead: this.playhead, segments: cloneSegments(this.segments) };
  }

  restore(snapshot) {
    this.selected = Math.max(0, Math.min(snapshot.selected, snapshot.segments.length - 1));
    this.playhead = Math.max(0, Math.min(this.duration, Number(snapshot.playhead) || 0));
    this.segments = cloneSegments(snapshot.segments);
  }

  commit(action, mutation) {
    const before = this.snapshot(action);
    const changed = mutation();
    if (!changed) return false;
    this.undoStack.push(before);
    if (this.undoStack.length > 50) this.undoStack.shift();
    this.redoStack.length = 0;
    return true;
  }

  undo() {
    const previous = this.undoStack.pop();
    if (!previous) return false;
    this.redoStack.push(this.snapshot(previous.action));
    this.restore(previous);
    return true;
  }

  redo() {
    const next = this.redoStack.pop();
    if (!next) return false;
    this.undoStack.push(this.snapshot(next.action));
    this.restore(next);
    return true;
  }

  setPlayhead(value) {
    this.playhead = Math.max(0, Math.min(this.duration, Number(value) || 0));
  }

  undoAction() {
    return this.undoStack.at(-1)?.action || '';
  }

  redoAction() {
    return this.redoStack.at(-1)?.action || '';
  }

  split(rawTime) {
    return this.commit('split', () => {
      const index = this.segments.findIndex((segment) => rawTime > segment.start + EPSILON && rawTime < segment.end - EPSILON);
      if (index < 0) return false;
      const current = this.segments[index];
      const left = { ...current, end: rawTime, fadeOut: 0 };
      const right = { ...current, start: rawTime, fadeIn: 0, name: '' };
      this.segments.splice(index, 1, left, right);
      this.selected = index + 1;
      return true;
    });
  }

  remove(index = this.selected) {
    return this.commit('remove', () => {
      if (this.segments.length <= 1 || !this.segments[index]) return false;
      this.segments.splice(index, 1);
      this.selected = Math.min(index, this.segments.length - 1);
      return true;
    });
  }

  trim(index, start, end) {
    return this.commit('trim', () => {
      const segment = this.segments[index];
      if (!segment) return false;
      const nextStart = Math.max(0, Math.min(start, segment.end - EPSILON));
      const nextEnd = Math.min(this.duration, Math.max(end, nextStart + EPSILON));
      if (Math.abs(nextStart - segment.start) < EPSILON && Math.abs(nextEnd - segment.end) < EPSILON) return false;
      segment.start = nextStart;
      segment.end = nextEnd;
      segment.fadeIn = Math.min(segment.fadeIn, (nextEnd - nextStart) / 2);
      segment.fadeOut = Math.min(segment.fadeOut, (nextEnd - nextStart) / 2);
      this.selected = index;
      return true;
    });
  }

  trimStartTo(index, rawTime) {
    const segment = this.segments[index];
    return segment ? this.trim(index, rawTime, segment.end) : false;
  }

  trimEndTo(index, rawTime) {
    const segment = this.segments[index];
    return segment ? this.trim(index, segment.start, rawTime) : false;
  }

  toggleFade(index, edge) {
    return this.commit(edge === 'in' ? 'fade in' : 'fade out', () => {
      const segment = this.segments[index];
      if (!segment) return false;
      const key = edge === 'in' ? 'fadeIn' : 'fadeOut';
      segment[key] = segment[key] > 0 ? 0 : Math.min(1, (segment.end - segment.start) / 2);
      this.selected = index;
      return true;
    });
  }

  rename(index, name) {
    return this.commit('rename', () => {
      const segment = this.segments[index];
      const clean = String(name || '').trim().slice(0, 80);
      if (!segment || segment.name === clean) return false;
      segment.name = clean;
      this.selected = index;
      return true;
    });
  }

  select(index) {
    if (!this.segments[index]) return false;
    this.selected = index;
    return true;
  }

  keptDuration() {
    return this.segments.reduce((total, segment) => total + segment.end - segment.start, 0);
  }

  editedFromRaw(rawTime) {
    let edited = 0;
    for (const segment of this.segments) {
      if (rawTime < segment.start) return edited;
      if (rawTime <= segment.end) return edited + rawTime - segment.start;
      edited += segment.end - segment.start;
    }
    return edited;
  }

  rawFromEdited(editedTime) {
    let cursor = 0;
    for (let index = 0; index < this.segments.length; index += 1) {
      const segment = this.segments[index];
      const length = segment.end - segment.start;
      if (editedTime <= cursor + length || index === this.segments.length - 1) {
        return { raw: Math.min(segment.end, segment.start + Math.max(0, editedTime - cursor)), index };
      }
      cursor += length;
    }
    return { raw: 0, index: 0 };
  }

  segmentAtRaw(rawTime) {
    return this.segments.findIndex((segment) => rawTime >= segment.start - EPSILON && rawTime <= segment.end + EPSILON);
  }

  isEdited() {
    const only = this.segments[0];
    return this.segments.length !== 1 || !only || only.start > EPSILON ||
      Math.abs(only.end - this.duration) > 0.05 || only.fadeIn > 0 || only.fadeOut > 0 || Boolean(only.name);
  }

  exportPlan() {
    return cloneSegments(this.segments);
  }
}
