import { Component, EventEmitter, Input, Output, OnDestroy, OnInit } from '@angular/core'
import { AbstractControl, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms'

import { HelperService } from '@k-services/svc.helper'
import { store, website } from '@k-settings/app-setup'

import { ConfiguratorService } from '../../services/svc.configurator'
import { ConfiguratorProductLoaderService } from '../../services/svc.configurator-product-reloader'

import { ReplaySubject, Subscription } from 'rxjs'
import { map, take } from 'rxjs/operators'
import { mediaServer } from '@k-settings/store-helper'

// Classes that should be external, but putting them for now
interface Measurement {[key: string]: {type: string; min?: number, max?: number; intervals?: Interval[];}}
class Interval {
    constructor(
        public minSelector: number,
        public maxSelector: number,
        public minDependant?: number,
        public maxDependant?: number,
        public dependant?: string,
    ) {}
}

enum MeasureType {
    'mm',
    'cm'
}

@Component({
    moduleId: module.id+ '',
    selector: 'configurator-measurements',
    templateUrl: './template/t--measurements.pug',
    styleUrls: ['sty.measurements.scss']
})
export class MeasurementsComponent implements OnInit, OnDestroy {

    //  ---- Variables ---- \\

    @Input('type') public type_id: string
    @Input() public position: number
    @Output() public setMeasurements: EventEmitter<{[key: string]: number} | false> = new EventEmitter()

    public data: Measurement[] | any
    private measurements: Measurement
    private _values: {[key: string]: number} = {}

    currentType: string
    hasIntervals: boolean = false
    // Observable subscriptions
    private subscriptions: Subscription = new Subscription()

    public prefix: string = this._configurator.configuratorType === 'curtain' ? '' : `${this._configurator.configuratorType}-`
    public activeIntervals: {[key: string]: {min: number, max: number}} = {}
    public formGroup: FormGroup = new FormGroup({})
    public currentMeasurementType: string

    typeMeasurementSource = new ReplaySubject<string>()
    typeMeasurement$ = this.typeMeasurementSource.asObservable()

    currentTypeSource = new ReplaySubject<string>()
    currentType$ = this.currentTypeSource.asObservable()

    constructor(
        private _configurator: ConfiguratorService,
        private preset: ConfiguratorProductLoaderService,
        private helper: HelperService
    ) {
        this.currentType = this.type_id

        if(store == 'bus') {
            this.currentMeasurementType = MeasureType[0]
        } else {
            this.currentMeasurementType = MeasureType[1]
        }
    }


    // ---- Lifecycle hooks ---- \\
    
    ngOnInit() {
        
        this.setMeasurements.emit(false)

        this.subscriptions.add(

            this.preset.preset$.pipe(take(1)).subscribe((response) => {

                if(response) {

                    setTimeout(() => {

                        this.currentType = response.type_id

                        if(this.currentType) {
                            this.typeMeasurementSource.next(`${mediaServer}/i/type_measurement/${this.currentType}/large/${website}`)
                        }
                        
                        if(response.measurements) {
                            for(let [key, measurement] of Object.entries(response.measurements)) {
                                this.formGroup.patchValue({[key]: key !== 'angle' && this.currentMeasurementType === MeasureType[0] ? this.convertMeasurements((measurement as number), MeasureType[0]) : measurement})
                            }
                        }
                    }, 400)
                }
            })
        )

        // Subscribe to type changes and change guide image accordingly
        this.subscriptions.add(
            this._configurator.product$.subscribe((response) => {

                this.currentType = response.type_id
                this.currentTypeSource.next(response.type_id)
                this.typeMeasurementSource.next(`${mediaServer}/i/type_measurement/${this.currentType}/large/${website}`)
            })
        )

        this.init()
    }

    // Deconstruct
    ngOnDestroy() {
        this.subscriptions.unsubscribe()
    }


    // ---- Functions ---- \\

    /**
     * Initializes the function, pulling the measurement data based on `type_id`
     * and creates a SourceMap of for `Data`, populates `this.data` with response
     * TODO: Rethink how to init
     */
    private init() {

        // Use input version type_id or currentType if type_id is undefined
        this._configurator.getMeasurements((!! this.type_id ? this.type_id : this.currentType ))
            .pipe(map((response) => response.measurements))
            .subscribe((response: Measurement) => {

                // if D exists, then define c and angle
                if('d' in response) {
                    response.c = {type: 'c', min: 0, max: 500}
                    response.angle = {type: 'angle', min: 0, max: 45}
                }
        
                // Define the local variables
                this.data = Object.values(response)
                this.measurements = response

                // Identify the validators
                this.setValidators(response)
            })


        // Field validation
        this.subscriptions.add(

            this.formGroup.valueChanges.subscribe(async (event: {[key: string]: number}) => {

                if(Object.values(event).every((value) => value === null)) {
                    this.resetIntervals().then(() => {

                        // Once done resetting, define new validators
                        this.setValidators(this.measurements)
                    })

                }


                // not this
                for await(let [key, value] of Object.entries(event)) {

                    if(!!value) {
                        this.formGroup.controls[key].markAsTouched() // Force error to display when field has been edited
                    }

                    // Isolate so only changed values gets looked at
                    if(this._values[key] !== value ) {
                        
                        if('intervals' in this.measurements[key]) {

                            let activeInterval: Interval | {} = this.getInterval({key: key, value: value}) || {}

                            if('dependant' in activeInterval) {
                                
                                this.setMinMax(activeInterval.minDependant, activeInterval.maxDependant, activeInterval.dependant)
                            } 
                        }

                        // Patch special values
                        if(key === 'a' || key === 'b' || key === 'd') {
                            this.formGroup.patchValue({c: +this.calculateC((event['d'] - event['b']), event['a']).toFixed((this.currentMeasurementType === MeasureType[0] ? 1 : 2))})

                            this.formGroup.controls['c'].updateValueAndValidity({emitEvent: false})
                            this.formGroup.controls['c'].markAsTouched() // Stop field from not showing validation
                        }
            
                        if(key === 'a' || key === 'c') {
                        
                            // let result = this._helper.rad2deg(Math.acos(this.resultMap.get('a') / this.resultMap.get('c')))) {
                            this.formGroup.patchValue({angle: +this.calculateAngle(event['a'], event['c']).toFixed(2)})

                            this.formGroup.controls['angle'].updateValueAndValidity({emitEvent: false})
                            this.formGroup.controls['angle'].markAsTouched() // Stop field from not showing validation
                        }
                    }
                }

                // Save the values so they can be compared
                this._values = event
            })
        )


        // On valid, expose the value, else send false, so validation fails
        this.subscriptions.add(

            this.formGroup.statusChanges.subscribe((event) => {
    
                if(event === 'VALID') {

                    let result = async (): Promise<{}> => {

                        let values: {[key: string]: string | number} = {}

                        if(this.formGroup.value) {
                            for await(let [key, value] of Object.entries(this.formGroup.value)) {
    
                                let _value: number = value as number
    
                                if(this.currentMeasurementType === MeasureType[0] && key !== 'angle') {
    
                                    values[key] = !_value ? null : Number(this.convertMeasurements(_value, MeasureType[1]))
                                } else {
                                    values[key] = !_value ? null : Number(_value)
                                }
    
                                // Decimal fix
                                if((values[key] as number) % 1) {
                                    values[key] = (values[key] as number).toFixed(2)
                                }
    
                                if(this.activeIntervals[key].min === this.activeIntervals[key].max) {
                                    delete values[key]
                                }
                            }
                        }

                        return values
                    }

                    result().then((results) => {
                        this.setMeasurements.emit(results)
                    })

                } else {

                    this.setMeasurements.emit(false)
                }
            })
        )
    }


    /**
     * Sets the validators
     */
    setValidators(response: Measurement) {

        if(response) {
            for(let [key, measurement] of Object.entries(response)) { // TODO: Fix that response.measurements comes as Object
    
                // Dynamically create formFields if they don't exist
                if(!this.formGroup.controls[measurement.type]) {
                    this.formGroup.addControl(measurement.type, new FormControl())
                }
    
                // Define the activeIntervals
                this.activeIntervals[measurement.type] = {min: this.currentMeasurementType === MeasureType[0] && measurement.type !== 'angle' ? this.convertMeasurements(measurement.min, MeasureType[0]) : measurement.min, max: this.currentMeasurementType === MeasureType[0] && measurement.type !== 'angle' ? this.convertMeasurements(measurement.max, MeasureType[0]) : measurement.max}
    
    
                // Base validator for regular fields
                if(!['c', 'angle', 'd'].includes(key)) {
                    this.formGroup.controls[key].setValidators([Validators.required, Validators.min(this.activeIntervals[measurement.type].min), Validators.max(this.activeIntervals[measurement.type].max)])
                }
                
                // Custom validators for specific fields
                if(key === 'd') {
                    this.formGroup.controls[key].setValidators([Validators.required, Validators.min(this.activeIntervals[measurement.type].min), Validators.max(this.activeIntervals[measurement.type].max), this.greaterThan('b')])
                }
    
                if(key === 'd') {
    
                    this.formGroup.addControl('c', new FormControl())
                    this.formGroup.addControl('angle', new FormControl())
    
                    this.formGroup.controls['c'].setValidators([Validators.required, Validators.max(this.currentMeasurementType === MeasureType[0] ? this.convertMeasurements(500, MeasureType[0]) : 500)]) // C is hardcoded to never expand above 500
    
                    this.formGroup.controls['angle'].setValidators([Validators.required, Validators.max(45)]) // Angle can never be above 45, this is a degree, and needs to be handled without metrics affecting it
                }
    
                if(this.activeIntervals[measurement.type].min === this.activeIntervals[measurement.type].max) {
                    this.formGroup.patchValue({[measurement.type]: this.activeIntervals[measurement.type].min}, {onlySelf: true})
                }
            }
        }
    }


    /**
     * Gets a specific interval based on the entity
     * 
     * @param entity 
     * @returns 
     */
    getInterval(entity) {

        let value = this.currentMeasurementType === MeasureType[0] ? this.convertMeasurements(entity.value, MeasureType[1]) : entity.value

        return this.measurements[entity.key].intervals.find((interval: Interval) => interval.minSelector <= value && interval.maxSelector >= value)
    }

    /**
     * Changes validation of a dependant
     * 
     * @param minSize 
     * @param maxSize 
     * @param dependant 
     */
    private setMinMax(minSize: number, maxSize: number, dependant: string): void {

        let min = this.currentMeasurementType === MeasureType[0] ? this.convertMeasurements(minSize, MeasureType[0]) : minSize
        let max = this.currentMeasurementType === MeasureType[0] ? this.convertMeasurements(maxSize, MeasureType[0]) : maxSize

        this.activeIntervals[dependant] = {min: min, max: max}

        // cleans validators before adding new
        this.formGroup.controls[dependant].clearValidators()

        // Set validity on field based on input
        this.formGroup.controls[dependant].setValidators([Validators.required, Validators.min(min), Validators.max(max)])

        if(this.formGroup.controls[dependant].value) {

            // Revalidate the inputs to ensure they behave accordingly
            this.formGroup.controls[dependant].updateValueAndValidity({emitEvent: false})
        }
    }


    /**
     * Resets all intervals
    */
    private async resetIntervals() {

        for await(let measurement of this.data) {

            if(this.formGroup.controls[measurement.type]) {

                this.formGroup.controls[measurement.type].clearValidators()

                this.formGroup.controls[measurement.type].markAsPristine()
                this.formGroup.controls[measurement.type].markAsUntouched()

                // Emit event to delete any marked errors
                this.formGroup.controls[measurement.type].updateValueAndValidity({emitEvent: false})
            }
        }
    }


    public onReset(): void {

        for(let measurement of this.data) {
            this.formGroup.patchValue({[measurement.type]: null})
        }
    }

    /**
     * Calculates C based on A and B values
     * 
     * @param sideA 
     * @param sideB 
     */
    private calculateC(sideA: number, sideB: number): number {
        return Math.sqrt(Math.pow(sideA, 2) + Math.pow(sideB, 2))
    }


    /**
     * Calculates the angle of the corner between A and C
     */
    private calculateAngle(sideA: number, sideC: number): number {
        return this.helper.rad2deg(Math.acos(sideA / sideC))
    }


    /**
     * Greater than validator. Checks if value of field is greater than input field
     * @param field 
     * @returns 
     */
    greaterThan(field: string): ValidatorFn {
        return (control: AbstractControl): {[key: string]: any} => {
            const group = control.parent;
            const fieldToCompare = group.get(field);
            const isLessThan = Number(fieldToCompare.value) < Number(control.value);

            return isLessThan ? {'lessThan': {value: control.value}} : null;
        }
    }


    convertMeasurements(value: number, toFormat: string) {

        switch(toFormat) {
            case MeasureType[0]:

                return value * 10

            case MeasureType[1]:

                return value / 10

            default:

                console.error(`type ${toFormat} does not exist!`)
                break;
        }
    }
}