/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import * as yaml from 'js-yaml';
import {Beans, CamelElement, Integration, Dependency} from "../model/IntegrationDefinition";
import {NamedBeanDefinition, RouteDefinition, ToDefinition} from "../model/CamelDefinition";
import {CamelUtil} from "./CamelUtil";
import {CamelDefinitionYamlStep} from "./CamelDefinitionYamlStep";
import {Kamelet} from "../model/KameletDefinition";
import {Annotations, Definition, Labels} from "../model/KameletModels";

export class CamelDefinitionYaml {

    static integrationToYaml = (integration: Integration): string => {
        const clone: any = CamelUtil.cloneIntegration(integration);
        const flows = integration.spec.flows
        clone.spec.flows = flows?.map((f: any) => CamelDefinitionYaml.cleanupElement(f)).filter(x => Object.keys(x).length !== 0);
        if (clone.spec.dependencies && Array.from(clone.spec.dependencies).length === 0) {
            delete clone.spec.dependencies;
        } else {
            clone.spec.dependencies = this.generateDependencies(clone.spec.dependencies);
        }
        if (integration.type === 'crd') {
            delete clone.type
            const i = JSON.parse(JSON.stringify(clone, null, 3)); // fix undefined in string attributes
            return CamelDefinitionYaml.yamlDump(i);
        } else {
            const f = JSON.parse(JSON.stringify(clone.spec.flows, null, 3));
            return CamelDefinitionYaml.yamlDump(f);
        }
    }

    static kameletToYaml = (kamelet: Kamelet): string => {
        const clone: any = CamelUtil.cloneKamelet(kamelet);
        if(clone.spec.template.from) {
            clone.spec.template.from = CamelDefinitionYaml.cleanupElement(clone.spec.template.from)
        }
        if (clone.spec.dependencies && Array.from(clone.spec.dependencies).length === 0) {
            delete clone.spec.dependencies;
        } else {
            clone.spec.dependencies = this.generateDependencies(clone.spec.dependencies);
        }
        if(clone.spec.template.beans && clone.spec.template.beans.length > 0) {
            const beans: NamedBeanDefinition[] = clone.spec.template.beans;
            clone.spec.template.beans = beans.map(bean => CamelDefinitionYaml.cleanupElement(bean)).filter(x => Object.keys(x).length !== 0);
        } else {
            delete clone.spec.template.beans;
        }
        const f = JSON.parse(JSON.stringify(clone, null, 3));
        return CamelDefinitionYaml.yamlDump(f);
    }

    static generateDependencies = (deps: Dependency[]): string[] => {
        let result: string[] = [];
        deps?.forEach(d => {
            let str: string = "";
            if(d.group && d.artifact) {
                str = `mvn:${d.group}:${d.artifact}`;
                if(d.version) {
                    str += `:${d.version}`
                }
                result.push(str)
            }
        })
        return result;
    }

    static cleanupElement = (element: CamelElement, inArray?: boolean, inSteps?: boolean): CamelElement => {
        const result: any = {};
        const object: any = Object.assign({}, element);
        if (inArray) {
            object.inArray = inArray;
            object.inSteps = (inSteps === true);
        }
        if (object.dslName?.endsWith('Expression')) {
            delete object.language;
            delete object.expressionName;
        } else if (object.dslName?.endsWith('DataFormat')) {
            delete object.dataFormatName;
        } else if (object.dslName === 'NamedBeanDefinition') {
            if (object.properties && Object.keys(object.properties).length === 0) delete object.properties;
        }
        delete object.uuid;
        delete object.show;
        Object.keys(object)
            .forEach(key => {
                if (object[key] instanceof CamelElement || (typeof object[key] === 'object' && object[key].dslName)) {
                    result[key] = CamelDefinitionYaml.cleanupElement(object[key])
                } else if (Array.isArray(object[key])) {
                    if (object[key].length > 0) result[key] = CamelDefinitionYaml.cleanupElements(object[key], key === 'steps')
                } else if (key === 'parameters' && typeof (object[key]) === 'object') {
                    const obj = object[key];
                    const parameters = Object.keys(obj || {}).reduce((x: any, k) => {
                        // Check for null or undefined or empty
                        if (obj[k] !== null && obj[k] !== undefined && obj[k].toString().trim().length > 0) {
                            x[k] = obj[k];
                        }
                        return x;
                    }, {});
                    if (Object.keys(parameters).length > 0) result[key] = parameters;
                } else {
                    if (object[key] !== undefined && object[key].toString().trim().length > 0) result[key] = object[key];
                }
            })
        return result as CamelElement
    }

    static cleanupElements = (elements: CamelElement[], inSteps?: boolean): CamelElement[] => {
        const result: any[] = []
        elements.forEach(element => {
            if (typeof (element) === 'object') {
                const newElement = CamelDefinitionYaml.cleanupElement(element, true, inSteps)
                result.push(newElement)
            } else {
                result.push(element);
            }
        })
        return result
    }

    static yamlDump = (integration: Integration): string => {
        return yaml.dump(integration,
            {
                noRefs: false,
                noArrayIndent: false,
                sortKeys: function (a: any, b: any) {
                    if (a === 'uri') return -1
                    else if (b === 'uri') return 1
                    else if (a === 'expression' && b == 'steps') return -1
                    else if (b === 'expression' && a == 'steps') return 1
                    else if (a === 'steps' && b !== 'uri') return -1
                    else if (b === 'steps' && a !== 'uri') return 1
                    else if (a > b) return 1
                    else return 0;
                },
                replacer: (key, value) => this.replacer(key, value)
            });
    }

    static replacer = (key: string, value: any): any => {
        if (typeof value === 'object' && (value.hasOwnProperty('stepName') || value.hasOwnProperty('inArray')  || value.hasOwnProperty('inSteps'))) {
            const stepNameField = value.hasOwnProperty('stepName') ? 'stepName' : 'step-name';
            const stepName = value[stepNameField];
            const dslName = value.dslName;
            let newValue: any = JSON.parse(JSON.stringify(value));
            delete newValue.dslName;
            delete newValue[stepNameField];
            if ((value.inArray && !value.inSteps)
                || dslName === 'ExpressionSubElementDefinition'
                || dslName === 'ExpressionDefinition'
                || dslName?.endsWith('Expression')
                || stepName === 'otherwise'
                || stepName === 'doFinally'
                || key === 'from') {
                delete newValue.inArray;
                delete newValue.inSteps;
                return newValue;
            } else {
                delete newValue.inArray;
                delete newValue.inSteps;
                const xValue: any = {};
                xValue[stepName] = newValue;
                return xValue;
            }
        } else {
            delete value?.dslName;
            return value;
        }
    }

    static yamlToIntegration = (filename: string, text: string): Integration => {
        const integration: Integration = Integration.createNew(filename);
        const fromYaml: any = yaml.load(text);
        const camelized: any = CamelUtil.camelizeObject(fromYaml);
        if (camelized?.apiVersion && camelized.apiVersion.startsWith('camel.apache.org') && camelized.kind && camelized.kind === 'Integration') {
            integration.type = 'crd';
            if (camelized?.metadata?.name) integration.metadata.name = camelized?.metadata?.name;
            const int: Integration = new Integration({...camelized});
            integration.spec.flows?.push(...this.flowsToCamelElements(int.spec.flows || []));
            integration.spec.dependencies = this.dependenciesToDependency(int.spec.dependencies);
        } else if (Array.isArray(camelized)) {
            integration.type = 'plain';
            const flows: any[] = camelized;
            integration.spec.flows?.push(...this.flowsToCamelElements(flows));
            integration.spec.dependencies = this.modelineToDependency(text);
        }
        return integration;
    }

    static yamlToKamelet = (filename: string, text: string): Kamelet => {

        const kamelet: Kamelet = Kamelet.createKamelet(filename);
        const fromYaml: any = yaml.load(text);
        const camelized: any = CamelUtil.camelizeObject(fromYaml);
        if(camelized?.metadata?.name) {
            kamelet.metadata.name = camelized?.metadata?.name;
        }
        if(camelized?.metadata?.labels) {
            kamelet.metadata.labels = new Labels(camelized?.metadata?.labels)
        }
        if(camelized?.metadata?.annotations) {
            kamelet.metadata.annotations = new Annotations(camelized?.metadata?.annotations)
        }
        const int: Kamelet = new Kamelet({...camelized});
        kamelet.spec.dependencies = this.dependenciesToDependency(int.spec.dependencies);
        kamelet.spec.definition = new Definition(int.spec.definition);
        kamelet.spec.template = this.templateToCamelElements(int.spec.template || {});
        return kamelet;
    }

    static yamlIsIntegration = (text: string): boolean => {
        const fromYaml: any = yaml.load(text);
        const camelized: any = CamelUtil.camelizeObject(fromYaml);
        if (camelized?.apiVersion && camelized.apiVersion.startsWith('camel.apache.org') && camelized.kind && camelized.kind === 'Integration') {
            return true;
        } else if (Array.isArray(camelized)) {
            return true;
        } else {
            return false;
        }
    }

    static dependenciesToDependency = (deps?: any[]): Dependency[] => {
        const result: Dependency[] = [];
        deps?.forEach((d: any) => result.push(Dependency.createNew(d.toString())));
        return result;
    }

    static modelineToDependency = (text: string): Dependency[] => {
        const result: Dependency[] = [];
        const lines = text.split("\r\n");
        lines.filter(l => {
            const line = l.trim();
            return line.startsWith("#") && line.includes("camel-k");
        }).forEach(line => {
            const parts = line.split(" ");
            parts.filter(part => part.trim() && part.trim().startsWith("dependency")).forEach(part => {
                const dep = part.replace("dependency=", '');
                result.push(Dependency.createNew(dep));
            })
        });
        return result;
    }

    static flowsToCamelElements = (flows: any[]): any[] => {
        const result: any[] = [];
        flows.filter((e: any) => e.hasOwnProperty('restConfiguration'))
            .forEach((f: any) => result.push(CamelDefinitionYamlStep.readRestConfigurationDefinition(f.restConfiguration)));
        flows.filter((e: any) => e.hasOwnProperty('rest'))
            .forEach((f: any) => result.push(CamelDefinitionYamlStep.readRestDefinition(f.rest)));
        flows.filter((e: any) => e.hasOwnProperty('route'))
            .forEach((f: any) => result.push(CamelDefinitionYamlStep.readRouteDefinition(f.route)));
        flows.filter((e: any) => e.hasOwnProperty('from'))
            .forEach((f: any) =>  result.push(CamelDefinitionYamlStep.readRouteDefinition(new RouteDefinition({from: f.from}))));
        flows.filter((e: any) => e.hasOwnProperty('beans'))
            .forEach((b: any) => result.push(CamelDefinitionYaml.readBeanDefinition(b)));
        return result;
    }

    static templateToCamelElements = (template: any): any => {
        const result: any = {};
        if(template.hasOwnProperty('beans')) {
            const beans: NamedBeanDefinition[] = [];
            template.beans.forEach((b: any) => {
                const props: any = {}
                if (b && b.properties) {
                    // convert map style to properties if requires
                    Object.keys(b.properties).forEach(key => {
                        const value = b.properties[key];
                        CamelDefinitionYaml.flatMapProperty(key, value, new Map<string, any>())
                            .forEach((v, k) => props[k] = v);
                    })
                }
                b.properties = props;
                beans.push(new NamedBeanDefinition(b))
            })
            result.beans = beans;
        }
        if(template.hasOwnProperty('from')) {
            result.from = CamelDefinitionYamlStep.readFromDefinition(template.from);
        }
        return result;
    }

    static readBeanDefinition = (beans: any): Beans => {
        const result: Beans = new Beans();
        beans.beans.forEach((b: any) => {
            const props: any = {}
            if (b && b.properties) {
                // convert map style to properties if requires
                Object.keys(b.properties).forEach(key => {
                    const value = b.properties[key];
                    CamelDefinitionYaml.flatMapProperty(key, value, new Map<string, any>())
                        .forEach((v, k) => props[k] = v);
                })
            }
            b.properties = props;
            result.beans.push(new NamedBeanDefinition(b))
        })
        return result;
    }

    // convert map style to properties if requires
    static flatMapProperty = (key: string, value: any, properties: Map<string, any>): Map<string, any> => {
        if (value === undefined) {
        } else if (typeof value === 'object') {
            Object.keys(value).forEach(k => {
                const key2 = key + "." + k;
                const value2: any = value[k];
                CamelDefinitionYaml.flatMapProperty(key2, value2, new Map<string, any>())
                    .forEach((value1, key1) => properties.set(key1, value1));
            })
        } else {
            properties.set(key, value);
        }
        return properties;
    }

}
