import { computed, observable, runInAction, makeObservable } from "mobx";
import { range } from "lodash";

import { UNITS } from "eez-studio-shared/units";

import type {
    IChart,
    IMeasurementFunction,
    IMeasureTask
} from "eez-studio-shared/extensions/extension";

import type {
    IMeasurement,
    IMeasurementsController,
    IMeasurementDefinition,
    ISingleInputMeasurementTaskSpecification,
    IMultiInputMeasurementTaskSpecification
} from "eez-studio-ui/chart/chart";

import { initValuesAccesor } from "eez-studio-ui/chart/value-accesor";

import { clamp } from "eez-studio-ui/chart/clamp";

export class Measurement implements IMeasurement {
    constructor(
        public measurementsController: IMeasurementsController,
        public measurementDefinition: IMeasurementDefinition,
        public measurementFunction: IMeasurementFunction | undefined
    ) {
        makeObservable(this, {
            dirty: observable,
            name: computed,
            result: observable
        });
    }

    dirty = true;

    get measurementId() {
        return this.measurementDefinition.measurementId;
    }

    get namePrefix() {
        return (
            (this.measurementFunction && this.measurementFunction.name) ||
            this.measurementDefinition.measurementFunctionId
        );
    }

    get name() {
        const namePrefix = this.namePrefix;

        let samePrefixBeforeCounter = 0;

        const measurements = this.measurementsController.measurements;

        let i;
        for (i = 0; i < measurements.length && measurements[i] !== this; ++i) {
            if (measurements[i].namePrefix === namePrefix) {
                samePrefixBeforeCounter++;
            }
        }

        if (i < measurements.length) {
            for (
                ++i;
                i < measurements.length &&
                measurements[i].namePrefix !== namePrefix;
                ++i
            ) {}
        }

        if (samePrefixBeforeCounter === 0 && i === measurements.length) {
            // no measurement with the same namePrefix found
            return namePrefix;
        }

        return `${namePrefix} ${samePrefixBeforeCounter + 1}`;
    }

    get arity() {
        return (
            (this.measurementFunction && this.measurementFunction.arity) || 1
        );
    }

    get parametersDescription() {
        return (
            this.measurementFunction &&
            this.measurementFunction.parametersDescription
        );
    }

    get parameters() {
        if (this.measurementDefinition.parameters) {
            return this.measurementDefinition.parameters;
        }

        const parameters: any = {};

        if (this.parametersDescription) {
            this.parametersDescription.forEach(parameter => {
                if (parameter.defaultValue) {
                    parameters[parameter.name] = parameter.defaultValue;
                }
            });
        }

        return parameters;
    }

    set parameters(value: any) {
        runInAction(() => (this.measurementDefinition.parameters = value));
    }

    get resultType() {
        return (
            (this.measurementFunction && this.measurementFunction.resultType) ||
            "value"
        );
    }

    get script() {
        return this.measurementFunction && this.measurementFunction.script;
    }

    get chartIndex() {
        return this.measurementDefinition.chartIndex || 0;
    }

    set chartIndex(value: number) {
        runInAction(() => (this.measurementDefinition.chartIndex = value));
    }

    get chartIndexes() {
        if (this.measurementDefinition.chartIndexes) {
            return this.measurementDefinition.chartIndexes;
        }
        return range(this.arity);
    }

    set chartIndexes(value: number[]) {
        runInAction(() => (this.measurementDefinition.chartIndexes = value));
    }

    getChartTask(
        chartIndex: number
    ): ISingleInputMeasurementTaskSpecification | null {
        if (!this.script) {
            return null;
        }

        const lineController =
            this.measurementsController.chartsController.lineControllers[
                chartIndex
            ];
        const waveformModel =
            lineController && lineController.getWaveformModel();

        if (!waveformModel) {
            return null;
        }

        const length: number = waveformModel.length;

        if (length === 0) {
            return null;
        }

        function xAxisValueToIndex(value: number) {
            return value * waveformModel!.samplingRate;
        }

        const { x1, x2 } = this.measurementsController.measurementsInterval!;

        let xStartValue: number = x1;
        let a: number = clamp(
            Math.floor(xAxisValueToIndex(x1)),
            0,
            waveformModel.length
        );
        let b: number = clamp(
            Math.ceil(xAxisValueToIndex(x2)),
            0,
            waveformModel.length - 1
        );

        const xNumSamples = b - a + 1;
        if (xNumSamples <= 0) {
            return null;
        }

        return {
            xStartValue: xStartValue,
            xStartIndex: a,
            xNumSamples,

            format: waveformModel.format,
            values: waveformModel.values,
            offset: waveformModel.offset,
            scale: waveformModel.scale,

            dlog: waveformModel.dlog,

            samplingRate: waveformModel.samplingRate,
            valueUnit: waveformModel.valueUnit
        };
    }

    getMeasureTaskForSingleInput(
        taskSpec: ISingleInputMeasurementTaskSpecification
    ) {
        const accessor = {
            format: taskSpec.format,
            values: taskSpec.values,
            offset: taskSpec.offset,
            scale: taskSpec.scale,
            dlog: taskSpec.dlog,
            length: 0,
            value: (value: number) => 0,
            waveformData: (value: number) => 0
        };

        initValuesAccesor(accessor, true);

        return {
            values: taskSpec.values,
            xStartValue: taskSpec.xStartValue,
            xStartIndex: taskSpec.xStartIndex,
            xNumSamples: taskSpec.xNumSamples,
            samplingRate: taskSpec.samplingRate,
            getSampleValueAtIndex: accessor.value,
            valueUnit: taskSpec.valueUnit,
            inputs: [],
            parameters: this.parameters,
            result: null
        };
    }

    getMeasureTaskForMultipleInputs(
        taskSpec: IMultiInputMeasurementTaskSpecification
    ) {
        const inputs = taskSpec.inputs.map(input => {
            const accessor = {
                format: input.format,
                values: input.values,
                offset: input.offset,
                scale: input.scale,
                dlog: input.dlog,
                length: 0,
                value: (value: number) => 0,
                waveformData: (value: number) => 0
            };

            initValuesAccesor(accessor, true);

            return {
                values: input.values,
                samplingRate: input.samplingRate,
                getSampleValueAtIndex: accessor.value,
                valueUnit: input.valueUnit
            };
        });

        return {
            values: null,
            xStartValue: taskSpec.xStartValue,
            xStartIndex: taskSpec.xStartIndex,
            xNumSamples: taskSpec.xNumSamples,
            samplingRate: taskSpec.inputs[0].samplingRate,
            getSampleValueAtIndex: inputs[0].getSampleValueAtIndex,
            valueUnit: inputs[0].valueUnit,
            inputs,
            parameters: this.parameters,
            result: null
        };
    }

    get task(): IMeasureTask | null {
        if (!this.script) {
            return null;
        }

        if (this.arity === 1) {
            const singleInputTaskSpec = this.getChartTask(this.chartIndex);
            if (!singleInputTaskSpec) {
                return null;
            }
            return this.getMeasureTaskForSingleInput(singleInputTaskSpec);
        }

        const tasks = this.chartIndexes
            .map(chartIndex => this.getChartTask(chartIndex))
            .filter(task => task) as ISingleInputMeasurementTaskSpecification[];

        if (tasks.length < this.arity) {
            return null;
        }

        const task = tasks[0];
        let xStartValue = task.xStartValue;
        let xStartIndex = task.xStartIndex;
        let xEndIndex = task.xStartIndex + task.xNumSamples;

        for (let i = 1; i < tasks.length; ++i) {
            const task = tasks[i];

            if (task.xStartIndex > xStartIndex) {
                xStartIndex = task.xStartIndex;
            }

            if (task.xStartIndex + task.xNumSamples < xEndIndex) {
                xEndIndex = task.xStartIndex + task.xNumSamples;
            }
        }

        const xNumSamples = xEndIndex - xStartIndex;
        if (xNumSamples <= 0) {
            return null;
        }

        return this.getMeasureTaskForMultipleInputs({
            xStartValue,
            xStartIndex,
            xNumSamples,

            inputs: tasks.map(task => ({
                format: task.format,
                values: task.values,
                offset: task.offset,
                scale: task.scale,
                samplingRate: task.samplingRate,
                valueUnit: task.valueUnit,
                dlog: task.dlog,
                getSampleValueAtIndex: (index: number) => 0
            }))
        });
    }

    result: {
        result: number | string | IChart | null;
        resultUnit?: keyof typeof UNITS | undefined;
    } | null = null;

    refreshResult() {
        if (!this.script) {
            return;
        }

        if (!this.measurementsController.measurementsInterval) {
            return;
        }

        const task = this.task;
        if (!task) {
            return;
        }

        let measureFunction: (task: IMeasureTask) => void;
        measureFunction = (require(this.script) as any).default;
        measureFunction(task);

        runInAction(() => {
            this.result = task;
            this.dirty = false;
        });
    }

    get chartPanelTitle() {
        const lineControllers =
            this.measurementsController.chartsController.lineControllers;
        if (lineControllers.length > 1) {
            if (this.arity === 1) {
                const lineController = lineControllers[this.chartIndex];
                if (lineController) {
                    return `${this.name} (${lineController.label})`;
                }
            } else {
                return `${this.name} (${this.chartIndexes
                    .map(chartIndex => {
                        const lineController = lineControllers[chartIndex];
                        return lineController ? lineController.label : "";
                    })
                    .join(", ")})`;
            }
        }

        return this.name;
    }
}
