/**
 * Made by mkcomponent.sh
*/

import { Component, OnInit, Input, Output, EventEmitter, ViewChild, Inject, ChangeDetectionStrategy, ElementRef } from '@angular/core'
import { DOCUMENT } from '@angular/common'

import { ReplaySubject, Subject, Observable } from 'rxjs'
import { map, tap, retry, mergeMap, delay } from 'rxjs/operators'
import { of } from 'rxjs/observable/of'
import { Subscription } from 'rxjs/Subscription'

import { store } from '@k-settings/app-setup'

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


// Enum
import { MountHeights } from './enum.mountHeights'
import { CustomBoolean } from './enum.customBoolean'

/**
```
0: 'mm',
1: 'cm'
```
 */
enum Measurement {
    'mm',
    'cm'
}

// Interfaces
interface chainElement {
    chain: number
    heights: number[] | string[]
    method?: number
    editable?: boolean
    min?: number
    max?: number
    presetLength?: number
}

interface IMountHeight {
    label: string
    editable: boolean
    calculate: boolean
}


interface IEmit {
    MountType: string
    mountPosition?: number
    mountHeights?: number
    customChain?: 'ja' | ''
}

@Component({
    moduleId: module.id+ '',
    selector: 'configurator-mountheight',
    templateUrl: './template/tpl--mount-height.pug',
    styleUrls: ['sty.mount-height.scss'],
    changeDetection: ChangeDetectionStrategy.OnPush
})



export class MountheightComponent implements OnInit {
    
    
    
    
    // ---- Variables ---- \\
    
    // Dependency Injection
    @Input('data') data: any
    @Input('position') position: number
    
    @Output() expose = new EventEmitter<IEmit>()

    @ViewChild('height') private _heightField: ElementRef//<HTMLElement>
    @ViewChild('dropdown') private _dropdownField: ElementRef//<HTMLSelectElement>

    // Component values
    selector: string = 'configurator-mountheight'
    height: number
    mountHeight: number
    private _mountHeight: number
    mountType: string
    confirm: boolean = false
    timeout: object = {}
    product: any
    presetLength: any = undefined
    selection: IMountHeight
    chainLength: chainElement = {
        chain: null,
        heights: []
    }

    public customMountHeight: number = 0
    public currentMeasurementType: string

    private _error: boolean = true
    private preset: any = false
    private useExternalMountType: boolean = false

    // Contain globalized intervals and timeouts
    intervals: any = {}

    editable: boolean = false
    
    // Observables
    private _subscriptions = new Subscription
    
    
    measurementSource = new ReplaySubject<IMountHeight>()
    measurement$ = this.measurementSource.asObservable()
    
    detectionSource = new Subject()
    detection$ = this.detectionSource.asObservable()
    





    constructor(
        @Inject(DOCUMENT) private _documentRef,
        private _service: ConfiguratorService,
        private _preset: ConfiguratorProductLoaderService
    ) {

        // Sets BUS scope to run "mm", while regular scope runs "cm"
        if(store === 'bus') {
            this.currentMeasurementType = Measurement[0]
        } else {
            this.currentMeasurementType = Measurement[1]
        }
        
        
        // Set preset values
        this._subscriptions.add(
            _preset.preset$
                .subscribe((response: any) => {

                    this._error = false
                    this.preset = response
                    this.product = response

                    
                    if(response.customChain === 'ja') {
                        this.confirm = true                        
                    }

                    if(response.MountType) {
                        this.setPresetType(response.MountType)
                    }
                    if(response.mountHeights) {

                        const _presetHeight = this.currentMeasurementType === Measurement[0] ? this.convertMeasurements(response.mountHeights, Measurement[0]) : response.mountHeights
                        this.setPresetHeight(_presetHeight)
                    }

                    if(response.details.mountPosition || response.mountPosition) {

                        let presetPosition = response.mountPosition || response.details.mountPosition
                        presetPosition = this.currentMeasurementType === Measurement[0] ? this.convertMeasurements(presetPosition, Measurement[0]) : presetPosition

                        this.mountHeight = presetPosition
                    }
                })
        )
        
        

        /**
         * Subscribe to product changes and update local product with response.
         * If product is different from local product, then trigger detection observable.
         */
        this._subscriptions.add(
            _service.product$
                .subscribe((response) => {
                    
                    // Run if product is not defined, or product is different from the current one in the service
                    const productIsDifferent        = !!this.product && (JSON.stringify(response).length != JSON.stringify(this.product).length)
                    const measurementsAreDifferent  = !!this.product && this.product.measurements && (response.measurements && response.measurements.height != JSON.parse(JSON.stringify(this.product)).measurements.height)
                    const mountHeightsDifferent     = !!this.product && (this.product.mountHeights !== response.mountHeights)
                    const mountPositionDifferent    = !!this.product && (this.product.mountPosition !== response.mountPosition)
                    const mountHeightNotNeededCheck = this.chainLength.method == 0 && (mountHeightsDifferent || mountPositionDifferent)

                    if(!this.product || (productIsDifferent || measurementsAreDifferent)) 
                    {    
                        // Do not update if method is 0 and mountheight properties are different
                        if(!mountHeightNotNeededCheck) {
                            setTimeout(() => {
                                this.detectionSource.next()
                            }, 100)
                        }
                    }

                    // response.mountPosition = this.currentMeasurementType === Measurement[0] ? this.convertMeasurements(response.mountPosition, Measurement[0]) : response.mountPosition
                    this.product = response
                })
        )
        


        /**
         * Subscribe to external changes to mounting method
         */
        this._subscriptions.add(
            _service.mountMethod$.subscribe((response: string | number) => {
                this.mountingMethod = response
            })
        )
        


        /**
         * Changes the type based on input
         */
        this._subscriptions.add(

            this.measurement$.subscribe((response: IMountHeight) => {

                if(!!response) {

                    // Save the current selection
                    this.selection = response
                    
                    // Expose the selected type through emitter
                    this.mountType = response.label
                    
                    setTimeout(() => {
                        this.detectionSource.next()
                    }, 50)
                }
            })
        )
        
        
        /**
         * Observable version of ngOnChanges() with different limitors and forcable triggers
         */
        this._subscriptions.add(

            this.detection$.subscribe(() => {

                // Predefine height is 0, as backend requires a value always
                let height: number = 0

                // Set height and mount height if type is custom
                if(this.mountType == MountHeights[2]) {
                    height = 100
                    this.mountHeight = this.mountHeight ? this.mountHeight : 0
                }


                // If product and product measurements exist, use these values to pull data from the request
                if(!!this.product && !!this.product.measurements) {

                    this.presetLength = undefined
                    height = this.product.measurements.height || (!!this.preset && !!this.preset.measurement && this.preset.measurements.height ? this.preset.measurements.height : undefined)
                }
                



                if(!!this.product && !!this.product.type_id) {

                    this._service.getChainLength(this.product.type_id, (height ? height : 0), this._mountHeight, (this.chainLength.method ? this.chainLength.method : ''))
                        .pipe(
                            map((response) => { return response.data }),
                            tap((response) => {
                                // console.log('response', response)
                                // console.log('this.product before detection', this.product)
                            }),
                            delay(500)
                        )
                        .subscribe({
                            next: (response) => {

                                // Skip this if value is custom
                                if(!this.confirm) {
                                    this.chainLength.chain = this.currentMeasurementType === Measurement[0] ? this.convertMeasurements(response.chain, Measurement[0]) : response.chain
                                }
                                
                                // Only set chainlength method if no external method has been set
                                if(!this.useExternalMountType) {
                                    this.chainLength.method     = response.method
                                }

                                this.chainLength.editable   = response.editable
                                this.chainLength.heights    = response.heights
                                this.chainLength.max        = this.currentMeasurementType === Measurement[0] ? this.convertMeasurements(response.max, Measurement[0]) : response.max
                                this.chainLength.min        = this.currentMeasurementType === Measurement[0] ? this.convertMeasurements(response.min < height ? height : response.min, Measurement[0]) : response.min < height ? height : response.min


                                // Failsafe ?
                                if(response.method && typeof this.chainLength.method === 'undefined') {
                                    this.chainLength.method = response.method
                                    this.detectionSource.next()

                                    throw new Error('method was not set, run again')
                                }


                                
                                let cuttoffPoint = 10
                                let productHeight = height

                                if(this.mountType === MountHeights[1] && (height <= cuttoffPoint || !this.mountHeight)) {
                                    this.resetChainLength()
                                }

                                // Only reset if within the Childsafe mount type
                                if(this.mountType === MountHeights[1] && productHeight >= cuttoffPoint) {

                                    if(productHeight > this.chainLength.min) {
                                        this.chainLength.min = productHeight
                                    }
                                }
            
                                if(!!this.preset) {
                                    this.presetLength = this.currentMeasurementType === Measurement[0] ? this.convertMeasurements(this.preset.mountHeights, Measurement[0]) : this.preset.mountHeights
                                }

            
                                // Clear chainLength on MountHeights[0], else emit the code
                                if(this.mountType == MountHeights[0]) {

                                    this.resetChainLength()
                
                                    this.emit()
                                    
                                } else {
                                    
                                    this.emit()
                                }
                            },
                            error: (e) => {
                                console.error(e)

                                this.resetChainLength()
                                this.chainLength.method = 0
                                this.emit()
                            }
                        })
                }

            })
        )
    }
    
    




    // ---- Lifecycle hooks ---- \\
    ngOnInit() {

        if(this.data.values) {    
            this.selected(this.data.values[0])
        }
    }
    
    
    ngOnDestroy() {
        this._subscriptions.unsubscribe()
    }
    
    
    // Handles

    onEdit() {

        this.editable = !this.editable

        if(!!this.preset && !!this.preset.mountHeights) {

            let int = setInterval(() => {

                if(!!this._documentRef.querySelector('.mountHeight-custom-input')) {
    
                    clearInterval(int)
                    
                    let elRef = this._documentRef.querySelector('.mountHeight-custom-input')
        
                    elRef.value = this.preset.mountHeights
                    this.preset = false

                }
            }, 50)


        } else {
            this._error = true
        }

        this.emit()
    }








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

    set mountingMethod(key) {
        this.chainLength.method = key

        // Set flag for external chainlength
        if(key === 'reset') {
            this.useExternalMountType = false
        }
        else {
            this.useExternalMountType = true
        }
        
        // re-run the loop with the new method
        this.detectionSource.next()

    }


    /**
     * Recovers the preset and sets it
     * 
     * @param type 
     */
    setPresetType(type) {
        
        let result = this.data.values.find((element) => {
            return element.label == type
        })
        
        this.selected(result)
    }
    
    
    /**
     * Sets chainLength to height and exposes chain length
     * 
     * @param height 
     */
    setPresetHeight(chainLength) {
    
        this.chainLength.chain = chainLength
        this._mountHeight = chainLength
        if(chainLength > 0) {

            this.emit()
        }

    }
    
    
    /**
     * Triggers the measurement source on any selection made
     * 
     * @param type 
     */
    selected(type) {
        this.measurementSource.next(type)
    }
    
    
    /**
     * Clears all selections, and sets error
     */
    resetChainLength() {

        this.chainLength.chain = null
        this.chainLength.heights = []

        this._error = true
    }


    /**
     * Emits the Mount Height selected. sets a timeout that clears itself it it's reposted before completing. Once completed
     * trigger a request to the detectionSource to pick up new info for the dropdown etc. 
     * 
     * @param element 
     */
    getMountHeight(event) {

        let inputValue = event.value
        this._mountHeight = this.currentMeasurementType === Measurement[0] ? this.convertMeasurements(event.value, Measurement[1]) : event.value


        if(!!this.preset) {
            this.preset = undefined
        }

        let elRef = this._heightField.nativeElement


        if(parseFloat(elRef.min) >= parseFloat(inputValue) || parseFloat(elRef.max) <= parseFloat(inputValue)) {

            elRef.classList.add('error')

            this._error = true
            this.emit()
            
        } else {

            elRef.classList.remove('error')
            this._error = false

            let value = parseInt(inputValue)

            if(this.intervals['getMountHeight'])
                clearTimeout(this.intervals['getMountHeight'])


            this.intervals['getMountHeight'] = setTimeout(() => {


                if(value > 0) {
                    this.detectionSource.next()

                } else {

                    this.resetChainLength()
                    this.emit()
                }
            }, 1200)
        }
    }


    
    /**
     * Exposes the customState based on the boolean state received
     * if state is returned as false, set the active type to the first one in the data array
     * 
     * @param state: boolean 
     */
    setCustomState(state: boolean) {
        
        if(state) {
            this.confirm = true

            // Once true, ensure to pull the newest data based on the choice made
            this.detectionSource.next()
            
        } else {
            this.measurementSource.next(this.data.values[0])
        }
    }
    


    /**
     * Triggers detection cycle on execution
     * TODO: Make more generic
     */
    edit() {
        this.detectionSource.next()
    }



    /**
     * Triggers detection cycle on execution
     * Blind reference to edit()
     * 
     * @param event - Never used?
     */
    setChainLength(event?) {
        this.edit()
    }



    /**
     * Modifies the result into an object used for validation. Based on the selections
     * made, the input values will change and modify how the end result looks
     * 
     * TODO: Reduce complexity
     */
    emit() {
        let values: IEmit = {
            MountType: this.mountType
        }

        // Model data based on selected inputs
        if(this.confirm) {
            values.customChain = CustomBoolean[this.confirm.toString()]
        }

        if(this._heightField) {
            values.mountPosition = this.currentMeasurementType === Measurement[0] ? this.convertMeasurements(parseInt(this._heightField.nativeElement.value), Measurement[1]) : parseInt(this._heightField.nativeElement.value)
        }


        // Logic based on which MountHeight type selected
        if(this.mountType == MountHeights[2]) {

            let dropdownRef = this._dropdownField ? this._dropdownField.nativeElement : undefined

            if(dropdownRef) {
                values.mountHeights = parseFloat(dropdownRef.value)
                this._error = false
            } else {
                
                let obs = new Observable((observer) => {

                    
                    observer.next(dropdownRef)

                }).pipe(
                    mergeMap(response => {
                        if(!response) {
                            throw('Error!')
                        }
                        return of(response)
                    }),
                    retry(5)
                    )
                .subscribe({
                    next: () => {
                        values.mountHeights = this.currentMeasurementType === Measurement[0] ? this.convertMeasurements(parseFloat(dropdownRef.value), Measurement[0]) : parseFloat(dropdownRef.value)
                    },
                    error: () => {
                        this._error = false
                    }
                })
            }
        } 


        // If value is equal to the secondary column, force mountPosition
        if(this.mountType == MountHeights[1]) {

            // Only force position and heights if they are needed
            if(this.chainLength.method !== 0) {
                values.mountPosition = this.mountHeight
                values.mountHeights = this.chainLength.chain
            }
            

            if(this._documentRef.querySelector('.mountHeight-custom-input') && !!this._documentRef.querySelector('.mountHeight-custom-input').value) {

                let elRef = this._documentRef.querySelector('.mountHeight-custom-input')
                this.customMountHeight = parseFloat(elRef.value)
                values.mountHeights =  this.customMountHeight

    
                // If value is not within the given span, force error and set value to 0, so product cannot be added to cart
                if(parseFloat(elRef.min) >= values.mountHeights || parseFloat(elRef.max) <= values.mountHeights) {
                    
                    elRef.classList.add('error')
                    this._error = true

                } else {    

                    elRef.classList.remove('error')
                    this._error = false
                }
            }
        }


        if(this.chainLength.method === 3 && this.mountType === MountHeights[1] && (this.mountHeight - ( this.customMountHeight ? this.customMountHeight : this.chainLength.chain)) < 0) {
            this._error = true
        }


        if(this.mountType == MountHeights[0]) {
            values.mountHeights = 100
            this._error = false
        }



        // if preset is defined, use that value instead
        if(!!this.presetLength && !this.editable) {
            values.mountHeights = this.presetLength
            this._error = false
        }


        // if _error is true, set value to 0, which causes invalid product
        if(this._error) {
            values.mountHeights = 0
        }


        // Remove unneeded properties from product if chainlength method is "Not needed"
        if(this.chainLength.method == 0) {
            this._error = false

            values = {
                MountType: 'Not needed',

                 // Set values to null, so that they are deleted from the product in the price component
                mountHeights: null,
                mountPosition: null
            }

            delete this.product.mountHeights
            delete this.product.mountPosition
        }



        if(this.currentMeasurementType === Measurement[0]) {


            // Loop through values object and make sure that values are numbers or null
            const forceNumber = async () => {

                let modifiedProductType = {}
    
                for await(let [key, value] of Object.entries(values)) {

                    if(!isNaN(value) && value !== null) {
                        modifiedProductType[key] = this.convertMeasurements(value, Measurement[1])
                    } else {
                        modifiedProductType[key] = value
                    }
                }

                return modifiedProductType
            }


            forceNumber().then((result: any) => {
                this.expose.emit(result)
            })

        } else {
            this.expose.emit(values)
        }

        // Expose the completed object, which will look slightly different depending on what type is selected
    }



    /**
     * Converts measurements between mm and cm
     * @param value 
     * @param toFormat 
     * @returns number - the modified measurement
     */
    convertMeasurements(value: number, toFormat: string): number {

        value = +value

        switch(toFormat) {
            case Measurement[0]:

                return value * 10

            case Measurement[1]:

                return value / 10

            default:

                console.error(`type ${toFormat} does not exist!`) // eslint-disable-line
                break
        }
    }
}