import { Injectable } from '@angular/core'
import { UntypedFormGroup, UntypedFormControl } from '@angular/forms'

import { Observable, BehaviorSubject, forkJoin, of as observableOf, Subject } from 'rxjs'
import { map } from 'rxjs/operators'

// import { Apollo, gql } from 'apollo-angular'

import { MapControlService } from '../map/map-control.service'
import { LayerNode } from '../map/models'
import { Tag, TagAction } from './info-tags/tags.service'

import { ConfigService } from '../_services/config.service'
import { DataService } from '../_services/data.service'
import { ApiService, ModelInfo, FeatureInfo, PlotParams } from '../openapi'

// const getModelInfoQuery = gql`
// query modelInfo ($store: String!) {
//     modelInfo(store: $store) {
//         store
//         modelName
//         symbol
//         values {
//             name
//             title
//             unit
//             chartType
//             chartColor
//         }
//         actions {
//             name
//             icon
//             formFields {
//                 name
//                 type
//             }
//         }
//         formName
//         formFields {
//             name
//             type
//         }
//         tagPlugins
//         tagActions {
//             domain
//             key
//             actions {
//                 plugin
//                 name
//                 link
//                 action
//                 roles
//             }
//         }
//         downloaders {
//             name
//             icon
//         }
//         legend {
//             key
//             value
//         }
//     }
// }
// `

// const getTagKeysQuery = gql`
// query tagKeyList {
//     tagKeyList {
//         keys
//     }
// }
// `

// const createTagMutation = gql`
// mutation createTag(
//     $store: String!
//     $id: String!
//     $key: String!
//     $value: String!
// ) {
//     createTag(
//         store: $store,
//         id: $id,
//         key: $key,
//         value: $value
//     ) {
//         tags {
//             key
//             value
//         }
//     }
// }
// `

// const createTagsMutations = gql`
// mutation createTags(
//     $keys: [String]!
//     $values: [String]!
//     $stores: [String]!
//     $ids: [[String]]!
// ) {
//     createTags(
//         keys: $keys,
//         values: $values,
//         stores: $stores,
//         ids: $ids,
//     ) {
//         tags {
//             key
//             value
//         }
//     }
// }
// `

// const getPlotParamsQuery = gql`
// query plotParams($store: String!, $id:String!, $value:String!) {
//     plotParams(store: $store, id: $id, value: $value) {
//         baseLines {
//             name
//             value
//             color
//         }
//         bgShapes {
//             name
//             valueTop
//             valueBottom
//             color
//         }
//         barBase
//     }
// }
// `

// const getFeatureInfoQuery = gql`
// query featureInfo($store: String!, $id:String!) {
//     featureInfo(store: $store, id: $id) {
//         id
//         itemName
//         geoInfoItems {
//             key
//             value
//         }
//         surveyInfoItems {
//             key
//             value
//         }
//         infoItems {
//             key
//             value
//         }
//         categorizedInfoItems {
//             name
//             infoItems {
//                 key
//                 value
//             }
//         }
//         tags {
//             key
//             value
//         }
//         graph
//         files {
//             name
//             path
//         }
//         images {
//             name
//             path
//         }
//         externalRecordUrl
//     }
// }
// `

// const deleteTagQuery = gql`
// mutation deleteTag(
//     $store: String!
//     $id: String!
//     $key: String!
// ) {
//     deleteTag(
//         store: $store,
//         id: $id,
//         key: $key,
//     ) {
//         tags {
//             key
//             value
//         }
//     }
// }
// `

// const getTaggedFeaturesQuery = gql`
// query taggedFeatures($stores: [String], $ids: [[String]]) {
//     taggedFeatures(stores: $stores, ids: $ids) {
//         store
//         taggedFeatures {
//             id
//             lon
//             lat
//             tags {
//                 key
//                 value
//             }
//         }
//     }
// }
// `

// const getTaggedStoresQuery = gql`
// query taggedStores($stores: [String]) {
//     taggedStores(stores: $stores) {
//         store
//         taggedFeatures {
//             id
//             lon
//             lat
//             tags {
//                 key
//                 value
//             }
//         }
//     }
// }
// `
// const executeFeatureActionMutation=gql`
// mutation executeFeatureAction (
//     $store: String!,
//     $id: String!,
//        $action: String!,
//        $value: String,
// ) {
//     executeFeatureAction(
//         store: $store,
//         id: $id,
//         action: $action,
//         value: $value,
//     ) {
//         result
//     }
// }
// `

export class TaggedFeature {
    constructor(
        public id: number,
        public lon: number,
        public lat: number,
        public tags: Tag[],
    ) {}
}

export class TaggedLayer {
    constructor(
        public store: string,
        public features: TaggedFeature[]
    ) {}
}

export class ModelAction {
    constructor(
        public name: string,
        public icon: string,
        public formFields: FormField[],
    ) {}

    /*
    execute(fullInfo: FullInfo) {
        // XXX: for downloads (reports) only
        // TODO: make actions generic (query, mutation, parameters...)
        window.open('/download/action/' + this.name + '/' + fullInfo.modelInfo.store + '/' + fullInfo.featureInfo.id)
    }
    */
}

export class FormFieldInput {
    constructor(
        public name: string,
        public value: string,
    ) {}
}

export class FormField {
    constructor(
        public name: string,
        public type: string,
        public dflt?: string,
        public value?: string
    ) {}
}

export class ModelValue {
    constructor(
        public name: string,
        public title: string,
        public unit: string,
        public chartType: string = 'line',
        public chartColor: string = 'blue',
    ) {}
}

// export class Downloader {
//     constructor(
//         public name: string,
//         public icon: string,
//     ) {}
// }

export class LegendItem {
    constructor(
        public key: string,
        public value: string,
    ) {}
}

// export class ModelInfo {
//     constructor(
//         public store: string,
//         public modelName: string,
//         public symbol: string,
//         public values: ModelValue[],
//         public actions: ModelAction[],
//         public formName: string,
//         public formFields: FormField[],
//         public tagPlugins: String[],
//         public tagActions: TagAction[],
//         public downloaders: Downloader[],
//         public legend: LegendItem[],
//     ) {}

//     getFormFields(formGroup: UntypedFormGroup, fullInfo: FullInfo): object[] {
//         // Return the form fields and build the FormGroup controls accordingly
//         let formFields = []
//         fullInfo.modelInfo.formFields.forEach(
//             field => {
//                 let control = new UntypedFormControl(field.name) //, field.validator)
//                 formGroup.addControl(field.name, control)
//                 formFields.push(field)
//             }
//         )
//         return formFields
//     }
// }

// export class PlotBaseLine {
//     constructor(
//         public name: string,
//         public value: number,
//         public color: string,
//     ) {}
// }

// export class PlotBgShape {
//     constructor(
//         public name: string,
//         public valueTop: number,
//         public valueBottom: number,
//         public color: string,
//     ) {}
// }

// export class PlotParams {
//     constructor(
//         public baseLines: PlotBaseLine[] = [],
//         public bgShapes: PlotBgShape[] = [],
//         public barBase?: number
//     ) {}
// }

export type PlotDataParams = {
    data: Object[],
    comment: string,
    params: PlotParams
}

export class InfoItem {
    constructor(
        public key: string,
        public value: string,
    ) {}
}

export class InfoCategory {
    constructor(
        public name: string,
        public infoItems: InfoItem[],
    ) {}
}

export class Attachment {
    constructor(
        public name: string,
        public path: string,
    ) {}
}

export class Feature {
    constructor (
        public store: string,
        public id: string,
    ) {}
}

export class FeatureWithField {
    constructor (
        public store: string,
        public field: string,
        public value: string,
    ) {}
}

// export class FeatureInfo {
//     constructor(
//         public id: string,
//         public itemName: string,
//         public geoInfoItems: InfoItem[],
//         public surveyInfoItems: InfoItem[],
//         public infoItems: InfoItem[],
//         public categorizedInfoItems: InfoCategory[],
//         public tags: Tag[],
//         public graph?: string,
//         public files?: Attachment[],
//         public images?: Attachment[],
//         public externalRecordUrl?: string
//     ) {}
//     openExternalRecord() {
//         window.open(this.externalRecordUrl)
//     }
// }

export class FullInfo {
    constructor(
        public modelInfo: ModelInfo,
        public featureInfo: FeatureInfo,
    ) {}

    hasForm(): Boolean {
        return this.modelInfo.formFields.length > 0
    }
}

@Injectable()
export class InfoDataService {
    constructor(
        public configService: ConfigService,
        public mapControlService: MapControlService,
        protected dataService: DataService,
        private api: ApiService,
    ) {}

    public refresh = new Subject<boolean>()
    public refresh$ = this.refresh.asObservable()

    public dataProviderService = new BehaviorSubject<FullInfo|undefined>(undefined)
    public dataProviderService$ = this.dataProviderService.asObservable()

    public taggedFeaturesSelectionService = new BehaviorSubject<TaggedLayer[]>([])
    public taggedFeaturesSelectionService$ = this.taggedFeaturesSelectionService.asObservable()

    // taggedLayers: holds the tags for each feature, for each selected layer, for search
    public taggedLayers = new BehaviorSubject<TaggedLayer[]>([])
    public taggedLayers$ = this.taggedLayers.asObservable()

    getModelInfo(store: string): Observable<ModelInfo> {
        return this.api.getModelInfoApiModelInfoStoreGet({store: store})
        console.warn('Migrate Graphql')
        return observableOf()
        // return this.apollo.query({
        //     query: getModelInfoQuery,
        //     variables: {
        //         store: store,
        //     }
        // }).pipe(map(
        //     res => {
        //         let info: Object = res['data']['modelInfo']
        //         let values = (info['values'] || []).map(
        //             (value: Object) => new ModelValue(
        //                 value['name'],
        //                 value['title'],
        //                 value['unit'],
        //                 value['chartType'],
        //                 value['chartColor'],
        //             )
        //         )
        //         return new ModelInfo(
        //             info['store'],
        //             info['modelName'],
        //             info['symbol'],
        //             values,
        //             info['actions'] ? info['actions'].map(
        //                 action => new ModelAction(
        //                     action['name'],
        //                     action['icon'],
        //                     action['formFields'].map(
        //                         formField => new FormField(
        //                             formField['name'],
        //                             formField['type'],
        //                             formField['dflt']
        //                         )
        //                     )
        //                 )
        //             ): [],
        //             info['formName'],
        //             info['formFields'] ? info['formFields'].map(
        //                 (formField: Object) => new FormField(
        //                     formField['name'],
        //                     formField['type'],
        //                 )
        //             ) : [],
        //             info['tagPlugins'],
        //             info['tagActions'] ? info['tagActions'].map(
        //                 tagAction => new TagAction(
        //                     // FIXME: set real data!!!
        //                     '**name**',
        //                     '**plugin_name**',
        //                     //tagAction['key'],
        //                     tagAction.actions[0]['action'],
        //                     ['**role**'],
        //                     '**link**',
        //                 )
        //             ) : [],
        //             info['downloaders'] ? info['downloaders'].map(
        //                 downloader => new Downloader(
        //                     downloader['name'],
        //                     downloader['icon'],
        //                 )
        //             ) : [],
        //             info['legend'] ? info['legend'].map(
        //                 legendItem => new LegendItem(
        //                     legendItem['key'],
        //                     legendItem['value'],
        //                 )
        //             ) : [],
        //         )
        //     }
        // ))
    }

    getPlotDataAndParams(store: string, id: string, value: string, resampling: string): Observable<PlotDataParams> {
        return forkJoin([
            this.dataService.getValues(store, +id, value, resampling),
            this.getPlotParams(store, id, value),
        ]).pipe(map(
            res => { return {
                data: JSON.parse(res[0][0]),
                // comment: res[0].headers['comment'],
                comment: res[0][1],
                params: res[1],
            }}
        ))
    }

    getPlotParams(store: string, id: string, value: string): Observable<PlotParams> {
        return this.api.getPlotParamsApiPlotParamsStoreGet({store: store, id: id, value: value})
        console.warn('Migrate Graphql')
        return observableOf()
        // return this.apollo.query({
        //     query: getPlotParamsQuery,
        //     variables: {
        //         store: store,
        //         id: id,
        //         value: value
        //     }
        // }).pipe(map(
        //     info => info.data['plotParams'] ? new PlotParams(
        //         (info.data['plotParams']['baseLines'] || []).map(
        //             bl => new PlotBaseLine(
        //                 bl['name'],
        //                 bl['value'],
        //                 bl['color']
        //             )
        //         ),
        //         (info.data['plotParams']['bgShapes'] || []).map(
        //             bl => new PlotBgShape(
        //                 bl['name'],
        //                 bl['valueTop'],
        //                 bl['valueBottom'],
        //                 bl['color']
        //             )
        //         ),
        //         info.data['plotParams']['barBase']
        //     ) : new PlotParams()
        // ))
    }

    getFeatureInfo(store: string, id: string): Observable<FeatureInfo> {
        return this.api.getFeatureInfoApiFeatureInfoStoreIdGet({store: store, id: id})
        // return this.apollo.query({
        //     query: getFeatureInfoQuery,
        //     variables: {
        //         store: store,
        //         id: id
        //     }
        // }).pipe(map(
        //     res => {
        //         const info = res['data']['featureInfo']
        //         const geoInfoItems = info['geoInfoItems'].map(ii => new InfoItem(ii['key'], ii['value']))
        //         const surveyInfoItems = info['surveyInfoItems'].map(ii => new InfoItem(ii['key'], ii['value']))
        //         const infoItems = info['infoItems'].map(ii => new InfoItem(ii['key'], ii['value']))
        //         const categorizedInfoItems = info['categorizedInfoItems'] && info['categorizedInfoItems'].map(
        //             ic => new InfoCategory(
        //                 ic['name'],
        //                 ic['infoItems'].map(ii => new InfoItem(ii['key'], ii['value']))
        //             )
        //         )
        //         const tags = info['tags'].map(ii => new Tag(ii['key'], ii['value']))

        //         return new FeatureInfo(
        //             info['id'],
        //             info['itemName'],
        //             geoInfoItems,
        //             surveyInfoItems,
        //             infoItems,
        //             categorizedInfoItems,
        //             tags,
        //             info['graph'] && info['graph'].replace(/width="\d+pt"/, '').replace(/height="\d+pt"/, ''),
        //             info['files'] && info['files'].map(att => new Attachment(att['name'], att['path'])),
        //             info['images'] && info['images'].map(att => new Attachment(att['name'], att['path'])),
        //             info['externalRecordUrl']
        //         )
        //     }
        // ))
    }

    getFullInfo(feature: Feature): Observable<FullInfo> {
        return forkJoin([
            this.getModelInfo(feature.store),
            this.getFeatureInfo(feature.store, feature.id)
        ]).pipe(map(
            res => new FullInfo(res[0], res[1])
        ))
    }

    public createTag(fullInfo: FullInfo, tag: any): Observable<Tag[]> {
        let variables = {
            'store': fullInfo.modelInfo.store,
            'id': fullInfo.featureInfo.id,
            'key': tag['key'],
            'value': tag['value'],
        }
        console.warn('Migrate Graphql')
        return observableOf()
        // return this.apollo.mutate({
        //     mutation: createTagMutation,
        //     variables: variables,
        // }).pipe(map(
        //     res => res['data']['createTag']['tags'].map(
        //         (tag: Object) => new Tag(tag['key'], tag['value']))
        // ))
    }

    public createTags(keys: String[], values: String[], source: Object): Observable<Tag[]> {
        let variables = {
            'keys': keys,
            'values': values,
            'stores': Object.keys(source),
            'ids': Object.values(source).map(ids => Array.from(<Number[][]>ids)),
        }
        console.warn('Migrate Graphql')
        return observableOf()
        // return this.apollo.mutate({
        //     mutation: createTagsMutations,
        //     variables: variables,
        // }).pipe(map(
        //     res => {
        //         let tags = res['data']['createTags']['tags']
        //         return tags.map(
        //             (tag: Object) => new Tag(tag['key'], tag['value']))
        //     }
        // ))
    }

    public deleteTag(tag: Tag | any, fullInfo?: FullInfo): Observable<object> {
        // tag can be FeatureTree, but circular dependencies:
        // import { FeatureTree } from './info-selection/info-selection-tags.component'
        let variables: Object
        if (!tag.id) {
            // A Tag, really
            variables = {
                'store': fullInfo.modelInfo.store,
                'id': fullInfo.featureInfo.id,
                'key': tag.key,
            }
        }
        else {
            // FeatureTree
            variables = {
                'store': tag.getStore(),
                'id': tag.id,
                'key': tag.getKey(),
            }
        }
        console.warn('Migrate Graphql')
        return observableOf()
        // return this.apollo.mutate({
        //     mutation: deleteTagQuery,
        //     variables: variables,
        // })
    }

    public getTagKeys(): Observable<string[]> {
        return observableOf(this.configService.conf.value.bsData?.map['tagKeys'])
        // This could be fetched from the server
        /*
        return this.apollo.query({
            query: getTagKeysQuery
        }).pipe(map(
            res => {
                return res['data']['tagKeyList']['keys']
            }
        ))
        */
    }

    private _getTaggedLayers(taggedItems: Object[]): TaggedLayer[] {
        /*
        Convert tagged features to tagged layers,
        putting the tag records in the graphql data structure
        */
        return taggedItems.map(
            layer => new TaggedLayer(
                layer['store'],
                layer['taggedFeatures'].map(
                    feature => new TaggedFeature(
                        feature['id'],
                        feature['lon'],
                        feature['lat'],
                        feature['tags']
                    )
                )
            )
        )
    }

    public getTaggedFeatures(features: Object): Observable<TaggedLayer[]> {
        let stores = Object.keys(features)
        if (stores.length == 0) {
            this.taggedFeaturesSelectionService.next([])
            return observableOf(<TaggedLayer[]>[])
        }
        let ids = Object.values(features).map(t => Array.from(t))
        console.warn('Migrate Graphql')
        return observableOf()
        // return this.apollo.query({
        //     query: getTaggedFeaturesQuery,
        //     variables: {
        //         stores: stores,
        //         ids: ids
        //     }
        // }).pipe(map(
        //     res => {
        //         let taggedLayers = this._getTaggedLayers(res['data']['taggedFeatures'])
        //         // Add features with no tag
        //         Object.entries(features).forEach(
        //             ([store, _features]) => {
        //                 let taggedFeatures = taggedLayers.find(s => s.store==store)
        //                 if (!taggedFeatures) {
        //                     taggedFeatures = new TaggedLayer(store, [])
        //                     taggedLayers.push(taggedFeatures)
        //                 }
        //                 let taggedFeaturesIds = taggedFeatures.features.map(tf => +tf.id)
        //                 let featureIdsNoTag = Array(..._features).filter(x => !taggedFeaturesIds.includes(x))
        //                 featureIdsNoTag.forEach(
        //                     id => {
        //                         taggedFeatures.features.push(new TaggedFeature(id, undefined, undefined, []))
        //                     }
        //                 )
        //             }
        //         )
        //         this.taggedFeaturesSelectionService.next(taggedLayers)
        //         return taggedLayers
        //     }
        // ))
    }

    public getTaggedStores(stores: string[]): Observable<TaggedLayer[]> {
        console.warn('Migrate Graphql')
        return observableOf()
        // return this.apollo.query({
        //     query: getTaggedStoresQuery,
        //     variables: {
        //         stores: stores,
        //     }
        // }).pipe(map(
        //     res => this._getTaggedLayers(res['data']['taggedStores'])
        // ))
    }

    public getTagsActionsStores(stores: string[]): Observable<TaggedLayer[]> {
        console.warn('Migrate Graphql')
        return observableOf()
        // return this.apollo.query({
        //     query: getTaggedStoresQuery,
        //     variables: {
        //         stores: stores,
        //     }
        // }).pipe(map(
        //     res => this._getTaggedLayers(res['data']['taggedStores'])
        // ))
    }

    // Load tags for selected layers
    public getTaggedLayers(selectedLayers: LayerNode[], forceReload: boolean=true): Observable<TaggedLayer[]> {
        let selectedLayersStores = selectedLayers.map(
            (l: LayerNode) => l.store
        )
        // FIXME: this.taggedLayers should be really an array
        let untaggedLayers = forceReload ? selectedLayersStores : selectedLayersStores.filter(
            (x: string) => !Object.keys(this.taggedLayers).includes(x)
        )
        if (untaggedLayers.length > 0) {
            return this.getTaggedStores(untaggedLayers).pipe(map(
                taggedLayers => {
                    // Update the known taggedLayers
                    for (let taggedLayer of taggedLayers) {
                        // FIXME: this.taggedLayers should be really an array
                        this.taggedLayers[taggedLayer.store] = taggedLayer
                    }
                    // Make sure that even layers without features with tags are remembered
                    for (let taggedLayer of untaggedLayers) {
                        if (!this.taggedLayers.hasOwnProperty(taggedLayer)) {
                            this.taggedLayers[taggedLayer] = new TaggedLayer(taggedLayer, [])
                        }
                    }
                    // Save to mapControlService behavior subject
                    this.taggedLayers.next(taggedLayers)
                    return taggedLayers
                }
            ))
        }
        else {
            return this.taggedLayers
        }
    }
}
