export type TimelinePixels = number;
export type TimelineSeconds = number;
export type TimelineBeats = number;
export type ClipContentSeconds = number;
export type ClipBeats = number;

export type TimeMapper<
  T1 extends StudioTimeValue,
  T2 extends StudioTimeValue,
> = (point: T1) => TimeTuple<T2> | null;

export type StudioTimeValue =
  | TimelinePixels
  | TimelineSeconds
  | TimelineBeats
  | ClipBeats
  | ClipContentSeconds;

export type TimeRange<T extends StudioTimeValue> = {
  start: T;
  end: T;
};

// [ValueIfApproachedFromLeft, ValueIfApproachedFromRight]
export type TimeTuple<T extends StudioTimeValue> = [T, T];

export type MappedSegment<
  T1 extends StudioTimeValue,
  T2 extends StudioTimeValue,
> = {
  sourceRange: TimeRange<T1>;
  targetRange: TimeRange<T2>;
  slope: number;
  leftAnchored: boolean;
};

export const getMappedSegment = <
  T1 extends StudioTimeValue,
  T2 extends StudioTimeValue,
>(
  sourceRange: TimeRange<T1>,
  targetRange: TimeRange<T2>,
  slope?: number
): MappedSegment<T1, T2> => {
  // Be permissive about mismatched finite/∞ endpoints; we'll compute a robust slope below.
  // Still forbid a fully open source interval.
  // (Target can be fully open if needed; the mapper/unmapper guard ranges.)
  if (!isFinite(sourceRange.start) && !isFinite(sourceRange.end)) {
    throw new Error('Start and end of source range cannot both be infinite');
  }
  const leftAnchored = isFinite(sourceRange.start);

  // Derive a slope that is stable with infinities.
  let computedSlope: number;
  if (typeof slope === 'number') {
    computedSlope = slope;
  } else {
    const num = (targetRange.end as number) - (targetRange.start as number);
    const den = (sourceRange.end as number) - (sourceRange.start as number);
    const numFinite = isFinite(num);
    const denFinite = isFinite(den);
    if (numFinite && denFinite) {
      computedSlope = num / den;
    } else if (!numFinite && denFinite) {
      // finite denom, infinite numer -> vertical mapping
      computedSlope = Math.sign(num) * Infinity;
    } else if (numFinite && !denFinite) {
      // infinite denom, finite numer -> horizontal mapping
      computedSlope = 0;
    } else {
      // both infinite; choose a sane default (1) to avoid NaN, preserve direction if possible
      const numSign = Math.sign(num);
      const denSign = Math.sign(den);
      computedSlope = numSign === 0 || denSign === 0 ? 1 : numSign / denSign; // ±1
    }
  }
  return {
    sourceRange,
    targetRange,
    slope: computedSlope,
    leftAnchored,
  };
};

export const pointInRange = <T extends StudioTimeValue>(
  point: T,
  range: TimeRange<T>
) => {
  if (point < range.start || point > range.end) {
    return false;
  }
  return true;
};

export const mapPointInSegment = <
  T1 extends StudioTimeValue,
  T2 extends StudioTimeValue,
>(
  point: T1,
  segment: MappedSegment<T1, T2>
): T2 | null => {
  if (!pointInRange(point, segment.sourceRange)) {
    return null;
  }
  if (segment.slope === Infinity) {
    return segment.targetRange.end;
  }
  if (segment.slope === 0) {
    return segment.targetRange.start;
  }
  if (segment.slope === -Infinity) {
    return segment.targetRange.start;
  }

  if (segment.leftAnchored) {
    return (segment.slope * (point - segment.sourceRange.start) +
      segment.targetRange.start) as T2;
  } else {
    return (segment.slope * (point - segment.sourceRange.end) +
      segment.targetRange.end) as T2;
  }
};

export const unmapPointInSegment = <
  T1 extends StudioTimeValue,
  T2 extends StudioTimeValue,
>(
  point: T2,
  segment: MappedSegment<T1, T2>
): T1 | null => {
  if (!pointInRange(point, segment.targetRange)) {
    return null;
  }
  if (segment.slope === Infinity) {
    return segment.sourceRange.end;
  }
  if (segment.slope === 0) {
    return segment.sourceRange.start;
  }
  if (segment.slope === -Infinity) {
    return segment.sourceRange.start;
  }
  if (segment.leftAnchored) {
    return ((point - segment.targetRange.start) / segment.slope +
      segment.sourceRange.start) as T1;
  } else {
    return ((point - segment.targetRange.end) / segment.slope +
      segment.sourceRange.end) as T1;
  }
};

export const getPointMapper = <
  T1 extends StudioTimeValue,
  T2 extends StudioTimeValue,
>(
  segments: MappedSegment<T1, T2>[]
) => {
  return (point: T1): TimeTuple<T2> | null => {
    let firstResult: T2 | null = null;
    for (const segment of segments) {
      const result = mapPointInSegment(point, segment);
      if (result === null) continue;
      if (point === segment.sourceRange.end) {
        firstResult = result;
      } else {
        return [firstResult ?? result, result];
      }
    }
    if (firstResult !== null) {
      return [firstResult, firstResult];
    }
    return null;
  };
};

export const getPointUnmapper = <
  T1 extends StudioTimeValue,
  T2 extends StudioTimeValue,
>(
  segments: MappedSegment<T1, T2>[]
) => {
  return (point: T2): TimeTuple<T1> | null => {
    let firstResult: T1 | null = null;
    for (const segment of segments) {
      const result = unmapPointInSegment(point, segment);
      if (result === null) continue;
      if (point === segment.targetRange.end) {
        firstResult = result;
      } else {
        return [firstResult ?? result, result];
      }
    }
    if (firstResult !== null) {
      return [firstResult, firstResult];
    }
    return null;
  };
};

export const invertSegments = <
  T1 extends StudioTimeValue,
  T2 extends StudioTimeValue,
>(
  segments: MappedSegment<T1, T2>[]
): MappedSegment<T2, T1>[] => {
  return segments.map((segment) => ({
    ...segment,
    sourceRange: segment.targetRange,
    targetRange: segment.sourceRange,
    slope: 1 / segment.slope,
  }));
};
