/**
 * Made by mkcomponent.sh
*/

import { Component, OnInit, OnDestroy, Input, Output, EventEmitter, ChangeDetectionStrategy, ChangeDetectorRef, Inject, PLATFORM_ID } from '@angular/core'
import { ISubscription } from 'rxjs/Subscription'

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

// Global services
import { HelperService } from '@k-services/svc.helper'

// Local services
import { ConfiguratorService } from '../../services/svc.configurator'
import { ConfiguratorProductLoaderService } from '../../services/svc.configurator-product-reloader'
import { from } from 'rxjs/observable/from'
import { of } from 'rxjs/observable/of'
import { mergeMap, exhaustMap, switchMap, concatMap, map, tap, delay, finalize, skipWhile } from 'rxjs/operators'
import { isPlatformBrowser } from '@angular/common'



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

export class DetailsComponent implements OnInit {

    // ---- Variables ---- \\
    @Input() data: any
    @Input('position') position: number
    @Output() setDetails: any = new EventEmitter
    values: any = {}
    product: any
    preset: any
    loadPreset: boolean = true
    width = undefined
    validGroups = new Map
    validDependencies = new Map

    selectedDetails: any = {}

    // Helpers
    mediaPath = this._helper.server + 'i/'
    website: string = website
    
    detailPriceMap = new Map
    details: any
    
    // Observable subscriptions
    private productSubscription: ISubscription
    private presetSubscription: ISubscription


    constructor(
        @Inject(PLATFORM_ID) private _platformId, 
        private _helper: HelperService,
        private _service: ConfiguratorService,
        private _preset: ConfiguratorProductLoaderService,
        private cdr: ChangeDetectorRef
    ) {

        if(isPlatformBrowser(this._platformId)) {

            this.productSubscription = _service.product$
                .pipe(skipWhile((val) => val === this.product )) // Made to limit the amount of requests, as this causes an endless loop
                .subscribe((response) => {
    
                this.product = response
    
                if(!!response.measurements) {
                    
                    // Determine that width can either be A or Width, Width has priority
                    let width = response.measurements.width || response.measurements.c
        
                    if(!!response && !!response.measurements && !!width && (this.width != width)) {
                        if(this.data && this.details) {
                            this.findWidthInDetails(width)
                        }
                    }
                }
            })
    
            this.presetSubscription = _preset.preset$.subscribe((response) => {
                this.preset = response
    
                // Calibrate as forced preset
                this.calibrate(true)
            })
    
            _preset.reExpose('details')
        }
    }



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

        this.details = JSON.parse(JSON.stringify(this.data.values))

        this.calibrate()
        this.validateGroup(this.details)
        this.validateValues()
    }

    ngOnDestroy() {
        if(isPlatformBrowser(this._platformId)) {
            this.productSubscription.unsubscribe()
            this.presetSubscription.unsubscribe()
        }
    }



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

    /**
     * Calibrates the values based if they are default or not. If the value is set
     * in the preset, it will be loaded, else the default value of a attribute will
     * be selected.
     * 
     * @param preset - if the preset is set, just put the type to what it's already set as
     */
    calibrate(preset: boolean = false) {

        // Isolate dependencies for detailGroup? TODO: Understand what's happening...
        for(let detailGroup of this.details) {

            for(let detailList of detailGroup.values) {

                if(!!detailList.dependencies && detailList.dependencies.length) {
                    detailGroup.dependencies = detailList.dependencies
                }
            }
        }

        // Iterate elements for the group
        for(let element of this.details) {
            
            if(preset) {
                // Logic for presetting default from presets
                if(!!this.preset && !!this.preset.details && !!this.preset.details[element.name]) {

                    for(let child of element.values) {

                        if(child.detail_id == this.preset.details[element.name]) {
                            
                            this.setChild(element.name, child.detail_id)

                            // Locate the parent container
                            let parentContainer = this.details.find((p) => p.name == element.name)
                    
                            // Isolate the mountheight method and verify it's not an empty string
                            if(new Set(parentContainer.values.map(child => child.mountheight_method)).size > 1) {
                    
                                this._setMethod(child)
                            }
                        }
                    }
                }
            } else {

                // Handle non-preset defaults
                for(let child of this._helper.deepClone(element.values).sort((a,b) => a.priority - b.priority)) {

                    // Check if the child has dependencies
                    if(child.dependencies.length && child.dependencies.some((dependency) => Object.values(this.values).includes(dependency))) {

                        this.values[element.name] = this._findByPriority(element)

                    } else if(!child.dependencies.length) {
                        this.values[element.name] = this._findByPriority(element)
                        break;
                    }                        
                }
            }
        }

        this.exposeArray()
    }


    /**
     * Finds and populates width based prices on details
     */
    findWidthInDetails(width) {

        this.width = width
        console.log(this.details, this.data)


        if(!!width && this.details) {

            for(let details of this.details) {
    
                for(let detail of details.values) {

                    if(detail.prices.length > 0) {

                        for(let prices of detail.prices) {
    
                            if(prices.min_width <= this.width && prices.max_width >= this.width) {
                                detail.price = prices.price
                            }
                        }
                    }
                }
            }
        }

        this.cdr.detectChanges()
    }


    /**
     * Resets all the presets
     */
    resetPresets() {
        for(let detail of Object.keys(this.preset.details)) {
            this.setChild(detail, this.preset.details[detail])

            this.exposeArray()
        }
    }


    /**
     * Sets the child based on `parent` and `value`. If the `mountheight_method` is not unique
     * on the parent, allow it to be sent to the mount-height component through observable
     * 
     * @param parent 
     * @param value 
     */
    setChild(parent, value) {

        this.values[parent] = value
        
        // Locate the value from data
        let parentContainer = this.details.find((p) => p.name == parent)

        let childContainer = parentContainer.values.find((child) => child.detail_id == value)

        // Isolate the mountheight method and verify it's not an empty string
        if(new Set(parentContainer.values.map(child => child.mountheight_method)).size > 1) {

            this._setMethod(childContainer)
        }
        

        of(this.validateGroup(this.details))
        .pipe(delay(500))
        .subscribe({
            complete: () => {

                this.values[parent] = value

                for(let group of this.details) {

                    // Filter over all the groups values (details)
                    let localeSet = group.values.filter((detail) => {
                        if(detail.dependencies) {
        
                            return detail.dependencies.some((dependency) => {
                                
                                if(dependency.includes('&&')) {
        
                                    let arr = dependency.split('&&')
                                    
                                    return arr.every((dep) => {
                                        return Object.values(this.values).includes(dep)
                                    })
        
                                } else {
        
                                    return Object.values(this.values).includes(dependency)
                                }
        
                            })
        
                        }
                    })
                    
                    
                    this._helper.deepClone(localeSet).sort((a,b) => a.priority - b.priority)
        
        
                    // If localeSet was found, select the first element and check it
                    if(!this.values[group.name] && localeSet.length) {
                        this.setChild(group.name, this._findActiveValue(localeSet).detail_id)
                    }
        
                }
            
                this.validateValues()
                this.exposeArray()
            }
        })
    }


    /**
     * Finds the default value with sort, and sets it in the `values` object
     */
    setDefaultBasedOnSize() {

        if(!!this.product.measurements) {

            let width = this.product.measurements.width
            
            for(let group of this.details) {
                

                let sorted = this._helper.deepClone(group.values).sort((a,b) => a.priority - b.priority)

                if(!!this._findActiveValue(sorted)) {

                    this.values[group.name] = this._findActiveValue(sorted).detail_id
                }
            }
        }
    }

    /**
     * Posts mountheight_method to service
     * @param child 
     */
    private _setMethod(child) {

        if(child.mountheight_method.toString()) {
            this._service.mountMethod = child.mountheight_method
        }
        else {
            this._service.mountMethod = 'reset'
        }
    }


    /**
     * Displays the detail based on the min/max width connected to it
     * @param group 
     * @param value 
     */
    checkSize(group, value) {

        let result: boolean = true

        if(!!this.product && !!this.product.measurements && !!this.width) {


            if((this.width < value.min_width && value.min_width > 0) || (this.width > value.max_width && value.max_width > 1)) {
                result = false

                if(this.values[group] == value.detail_id) {
                    
                    // this.values[group] = undefined
                    this.setDefaultBasedOnSize()

                    this.exposeArray()
                }
            }
        } else if(!!this.width && (!!value.min_width && !!value.max_width)) {
            result = false

        } else {

            result = true
        }

        return result
    }


    /**
     * Validates an array of groups and puts them in the validGroups map
     * 
     * @param detailGroup 
     */
    validateGroup(detailGroup) {

        for(let group of detailGroup) {

            let groupState = []

            from(group.values)
                .pipe(

                    concatMap(item => of(this.checkDependencies(item, group))),
                    tap((response) => {
                        groupState.push(response)
                    })
                )
                .subscribe({

                    next: () => {
                        this.validGroups.set(group.group, groupState.some((r) => !!r))
                    }
                })
        }
    }


    /**
     * taps `this.values` to serve for `_validateValue`
     * 
     * @todo potentially have a look at making `this.values` into an Observable and then execute this function onChanges
     */
    public validateValues(): void {
        for(let [key, value] of Object.entries(this.values)) {

            // TypeCast value to always be string
            this._validateValue(key, (value as string))
        }
    }


    /**
     *  Validates key:value pair up against the selection, if none are selected, find the prioritized choice
     * If an invalid element is selected, delete and find a valid choice
     * 
     * @diagram https://drive.google.com/file/d/1CeUizYA9l4thXX_Flw6nIyUvRf6xsrWJ/view?usp=sharing
     * @param key 
     * @param value 
     */
    private _validateValue(key: string, value: string): void {

        // Check value => set / not set
        if(value) {

            let valid = true
            let dependencies: string[] = []

            // Find group, then find the selected attribute
            const groupMap = this.details.filter((group) => {

                if(group.name == key) {
                    return group
                }
            }).filter((group) => group.values.find((detail) => detail.detail_id == value))
            

            // Isolate the dependencies across all applicable groups
            groupMap.forEach((group) => {

                group.values.every((detail) => {

                    if('dependencies' in detail) {
                        dependencies = [...detail.dependencies, ...dependencies]
                    }
                })
            })


            // Checks if dependencies has a length, and then checks the validity of each dependency
            // ? https://stackoverflow.com/questions/53033854/why-does-the-argument-for-array-prototype-includessearchelement-need-the-same - SearchElement has the wrong definition in TypeScript, and fails if arrayTypes do not much, and sometimes even if they do, enforce any type to ignore
            if(!!dependencies.length ? dependencies.some((dependency) => {

                if(dependency.includes('&&')) {
                    return dependency.split('&&').every((d) => Object.values(this.values).includes(d))
                } else {
                    return Object.values(this.values).includes(dependency)
                }
            }) : true) {
                valid = true
            } else {

                valid = false
            }

            // if invalid, delete it, and re-run self to set, check if valid => invalid
            if(!valid) {

                delete this.values[key]
                this._validateValue(key, undefined)
            }

            // Skip further action, if value is valid

        } else {

            // Handle set of valid choice and self-run
            let group = this.details.find((group) => {
                if(group.name == key && this.validGroups.get(group.group)) {
                    return group
                }
            })

            if(!!group && !!group.values) {

                let newValue = this._helper.deepClone(group.values).sort((a, b) => a.priority - b.priority)
    
                
                if('dependencies' in newValue[0]) {

                    let activeValue = this._findActiveValue(newValue)

                    newValue = activeValue.detail_id

                } else {
                    newValue = newValue[0].detail_id
                }


                this.setChild(key, newValue)
                // this._validateValue(key, newValue)
            }
        }
    }

    
    /**
     * Checks the validity of a dependency
     * 
     * @param detail 
     */
    checkDependencies(detail, group) {
        
        let result = true

        // Isolate details with dependencies
        if(detail.dependencies.length) {

            result = detail.dependencies.some((dependency) => {

                if(dependency.includes('&&')) {

                    return dependency.split('&&').every((dep) => {

                        return Object.values(this.values).includes(dep)
                    })
                } else {

                    return Object.values(this.values).includes(dependency)
                }
            })
        }


        this.validDependencies.set(detail.detail_id, result)

        return result
    }


    /**
     * (Crudely) Posts values to parent
     */
    exposeArray() {


        let promise = new Promise((resolve, reject) => {
            let value = setInterval(() => {
                clearInterval(value)
                resolve(this.values)
            }, 200)

        }).then((response) => {

            for(let detailGroup of this.details) {

                if(this.validGroups.get(detailGroup.group) && detailGroup.dependencies) {
                    let result = true



                    if(detailGroup.dependencies.toString().includes('&&')) {

                        result = detailGroup.dependencies.some((dependency) => 
                            dependency.split('&&').some((dep) => 
                                Object.values(this.values).includes(dep)
                            )
                        )

                    } else {

                        result = detailGroup.dependencies.some((el) =>
                            Object.values(this.values).includes(el)
                        )

                    }

                    if(!result)
                        delete response[detailGroup.name]
                }
            }

            return response

        }).then((result) => {
            this.setDetails.emit(result)
        })
    }


    /**
     * Filters out unwanted chars from blockname
     * and returns filtered name
     * 
     * @param blockname 
     */
    filterBlockName(blockname: string) {
        return blockname.replace(/(?![a-zA-Z ])./g, '').replace(/ $/g, '').replace(/ /g, '-')
    }


    /**
    * Set Type (Type, Mål, color, Detailer)
    */
	setSelector(selector_id) {
		this._service.setSpecSelector(selector_id)
	}


    /**
     * Checks if detail has a dependency, and only display if it does
     * ! DEPRECATED
     * 
     * @param detail 
     */
    checkForDependencies(detail) {
        console.error('The function "checkForDependencies" is deprecated, please rewrite your code to not use this')
    }

    /**
     * Priority lookup helper
     * 
     * @param group 
     */
    private _findByPriority(group) {
        return this._findActiveValue(this._helper.deepClone(group.values).sort((a,b) => a.priority - b.priority)).detail_id
    }

    /**
     * Helper to identify active detail from list of details
     * 
     * @param value 
     */
    private _findActiveValue(value) {
        return this._helper.deepClone(value).sort((a,b) => a.priority - b.priority).find((detail) => {

            let detailLocator = detail.dependencies.every((dependency) =>  Object.values(this.values).includes(dependency))

            if(!detailLocator) {
                detailLocator = detail.dependencies.some((dependency) =>  {

                    if(dependency.includes('&&')) {
                        return dependency.split('&&').some((d) => Object.values(this.values).includes(d))
                    } else {
                        return Object.values(this.values).includes(dependency)
                    }
                })

            }


            let hasWidth = ('min_width' in detail) && 'max_width' in detail
            let hasWidthCheck = hasWidth ? (!!this.width ? detail.min_width <= this.width && detail.max_width >= this.width : true) : true

            // The code gets upset if there is only 1 element in value, so force set true no matter result, if that is the case
            return (value.length == 1 ? true : detailLocator && hasWidthCheck)
        })
    }
}
