import { BitmapCoordinatesRenderingScope, CanvasRenderingTarget2D } from 'fancy-canvas';
import {
	AutoscaleInfo,
	Coordinate,
	IChartApi,
	ISeriesApi,
	ISeriesPrimitive,
	ISeriesPrimitivePaneRenderer,
	ISeriesPrimitivePaneView,
	Logical,
	SeriesOptionsMap,
	SeriesType,
	Time,
	SeriesAttachedParameter,
	PrimitiveHoveredItem,
} from 'lightweight-charts';

class TrendLinePaneRenderer implements ISeriesPrimitivePaneRenderer {
	_p1: ViewPoint;
	_p2: ViewPoint;
	_text1: string;
	_text2: string;
	_options: TrendLineOptions;
	_selected: boolean;

	constructor(p1: ViewPoint, p2: ViewPoint, text1: string, text2: string, options: TrendLineOptions, selected: boolean) {
		this._p1 = p1;
		this._p2 = p2;
		this._text1 = text1;
		this._text2 = text2;
		this._options = options;
		this._selected = selected;
	}

	draw(target: CanvasRenderingTarget2D) {
		target.useBitmapCoordinateSpace(scope => {
			if (
				this._p1.x === null ||
				this._p1.y === null ||
				this._p2.x === null ||
				this._p2.y === null
			)
				return;
			const ctx = scope.context;
			const x1Scaled = Math.round(this._p1.x * scope.horizontalPixelRatio);
			const y1Scaled = Math.round(this._p1.y * scope.verticalPixelRatio);
			const x2Scaled = Math.round(this._p2.x * scope.horizontalPixelRatio);
			const y2Scaled = Math.round(this._p2.y * scope.verticalPixelRatio);
			
			// Apply preview styling if needed
			const lineWidth = this._options.isPreview ? this._options.width : (this._selected ? this._options.width * 1.5 : this._options.width);
			const opacity = this._options.isPreview ? 0.5 : 1.0;
			
			ctx.lineWidth = lineWidth;
			ctx.strokeStyle = this._options.lineColor;
			ctx.globalAlpha = opacity;
			
			// Apply dashed style for preview
			if (this._options.isPreview) {
				ctx.setLineDash([5 * scope.horizontalPixelRatio, 5 * scope.horizontalPixelRatio]);
			} else {
				ctx.setLineDash([]);
			}
			
			ctx.beginPath();
			ctx.moveTo(x1Scaled, y1Scaled);
			ctx.lineTo(x2Scaled, y2Scaled);
			ctx.stroke();
			
			ctx.setLineDash([]);
			ctx.globalAlpha = 1.0;
			
			// Draw endpoint handles when selected
			if (this._selected) {
				const handleRadius = 6 * scope.horizontalPixelRatio;
				ctx.fillStyle = 'white';
				ctx.strokeStyle = this._options.lineColor;
				ctx.lineWidth = 2 * scope.horizontalPixelRatio;
				
				// Handle at p1
				ctx.beginPath();
				ctx.arc(x1Scaled, y1Scaled, handleRadius, 0, 2 * Math.PI);
				ctx.fill();
				ctx.stroke();
				
				// Handle at p2
				ctx.beginPath();
				ctx.arc(x2Scaled, y2Scaled, handleRadius, 0, 2 * Math.PI);
				ctx.fill();
				ctx.stroke();
			}
			
			if (this._options.showLabels) {
				this._drawTextLabel(scope, this._text1, x1Scaled, y1Scaled, true);
				this._drawTextLabel(scope, this._text2, x2Scaled, y2Scaled, false);
			}
		});
	}

	_drawTextLabel(scope: BitmapCoordinatesRenderingScope, text: string, x: number, y: number, left: boolean) {
		scope.context.font = '24px Arial';
		scope.context.beginPath();
		const offset = 5 * scope.horizontalPixelRatio;
		const textWidth = scope.context.measureText(text);
		const leftAdjustment = left ? textWidth.width + offset * 4 : 0;
		scope.context.fillStyle = this._options.labelBackgroundColor;
		scope.context.roundRect(x + offset - leftAdjustment, y - 24, textWidth.width + offset * 2,  24 + offset, 5);
		scope.context.fill();
		scope.context.beginPath();
		scope.context.fillStyle = this._options.labelTextColor;
		scope.context.fillText(text, x + offset * 2 - leftAdjustment, y);
	}
}

interface ViewPoint {
	x: Coordinate | null;
	y: Coordinate | null;
}

class TrendLinePaneView implements ISeriesPrimitivePaneView {
	_source: TrendLine;
	_p1: ViewPoint = { x: null, y: null };
	_p2: ViewPoint = { x: null, y: null };

	constructor(source: TrendLine) {
		this._source = source;
	}

	update() {
		const series = this._source._series;
		const y1 = series.priceToCoordinate(this._source._p1.price);
		const y2 = series.priceToCoordinate(this._source._p2.price);
		const timeScale = this._source._chart.timeScale();
		const x1 = timeScale.timeToCoordinate(this._source._p1.time);
		const x2 = timeScale.timeToCoordinate(this._source._p2.time);
		this._p1 = { x: x1, y: y1 };
		this._p2 = { x: x2, y: y2 };
	}

	renderer() {
		return new TrendLinePaneRenderer(
			this._p1,
			this._p2,
			'' + this._source._p1.price.toFixed(1),
			'' + this._source._p2.price.toFixed(1),
			this._source._options,
			this._source._selected
		);
	}
}

interface Point {
	time: Time;
	price: number;
}

export interface TrendLineOptions {
	lineColor: string;
	width: number;
	showLabels: boolean;
	labelBackgroundColor: string;
	labelTextColor: string;
	isPreview?: boolean;
	annotationId?: string;
}

const defaultOptions: TrendLineOptions = {
	lineColor: 'rgb(0, 0, 0)',
	width: 2,
	showLabels: false,
	labelBackgroundColor: 'rgba(255, 255, 255, 0.85)',
	labelTextColor: 'rgb(0, 0, 0)',
};

export class TrendLine implements ISeriesPrimitive<Time> {
	_chart: IChartApi;
	_series: ISeriesApi<keyof SeriesOptionsMap>;
	_p1: Point;
	_p2: Point;
	_paneViews: TrendLinePaneView[];
	_options: TrendLineOptions;
	_minPrice: number;
	_maxPrice: number;
	_selected: boolean = false;
	_attachedParam: SeriesAttachedParameter<Time> | null = null;

	constructor(
		chart: IChartApi,
		series: ISeriesApi<SeriesType>,
		p1: Point,
		p2: Point,
		options?: Partial<TrendLineOptions>
	) {
		this._chart = chart;
		this._series = series;
		this._p1 = p1;
		this._p2 = p2;
		this._minPrice = Math.min(this._p1.price, this._p2.price);
		this._maxPrice = Math.max(this._p1.price, this._p2.price);
		this._options = {
			...defaultOptions,
			...options,
		};
		this._paneViews = [new TrendLinePaneView(this)];
	}

	updatePoints(p1: Point, p2: Point) {
		this._p1 = p1;
		this._p2 = p2;
		this._minPrice = Math.min(this._p1.price, this._p2.price);
		this._maxPrice = Math.max(this._p1.price, this._p2.price);
	}

	getP1(): Point {
		return this._p1;
	}

	getP2(): Point {
		return this._p2;
	}

	autoscaleInfo(startTimePoint: Logical, endTimePoint: Logical): AutoscaleInfo | null {
		const p1Index = this._pointIndex(this._p1);
		const p2Index = this._pointIndex(this._p2);
		if (p1Index === null || p2Index === null) return null;
		if (endTimePoint < p1Index || startTimePoint > p2Index) return null;
		return {
			priceRange: {
				minValue: this._minPrice,
				maxValue: this._maxPrice,
			},
		};
	}

	updateAllViews() {
		this._paneViews.forEach(pw => pw.update());
	}

	paneViews() {
		return this._paneViews;
	}

	_pointIndex(p: Point): number | null {
		const coordinate = this._chart
			.timeScale()
			.timeToCoordinate(p.time);
		if (coordinate === null) return null;
		const index = this._chart.timeScale().coordinateToLogical(coordinate);
		return index;
	}

	attached(param: SeriesAttachedParameter<Time>): void {
		this._attachedParam = param;
	}

	detached(): void {
		this._attachedParam = null;
	}

	setSelected(isSelected: boolean): void {
		this._selected = isSelected;
		this._series.applyOptions({});
	}

	hitTest(x: number, y: number): PrimitiveHoveredItem | null {
		if (!this._attachedParam) return null;

		const timeScale = this._chart.timeScale();
		const x1 = timeScale.timeToCoordinate(this._p1.time);
		const x2 = timeScale.timeToCoordinate(this._p2.time);
		const y1 = this._series.priceToCoordinate(this._p1.price);
		const y2 = this._series.priceToCoordinate(this._p2.price);

		if (x1 === null || x2 === null || y1 === null || y2 === null) {
			return null;
		}

		// Calculate perpendicular distance from point to line segment
		const distance = this._distanceToSegment(x, y, x1, y1, x2, y2);
		const tolerance = 10; // 10 CSS pixels

		if (distance <= tolerance) {
			return {
				cursorStyle: 'pointer',
				externalId: this._options.annotationId || '',
				zOrder: 'normal',
			};
		}

		return null;
	}

	_distanceToSegment(px: number, py: number, x1: number, y1: number, x2: number, y2: number): number {
		const dx = x2 - x1;
		const dy = y2 - y1;
		const lengthSquared = dx * dx + dy * dy;

		if (lengthSquared === 0) {
			// Line segment is a point
			return Math.sqrt((px - x1) * (px - x1) + (py - y1) * (py - y1));
		}

		// Project point onto line segment
		let t = ((px - x1) * dx + (py - y1) * dy) / lengthSquared;
		t = Math.max(0, Math.min(1, t));

		const projX = x1 + t * dx;
		const projY = y1 + t * dy;

		return Math.sqrt((px - projX) * (px - projX) + (py - projY) * (py - projY));
	}
}
