/*
 * Copyright 2021, The Android Open Source Project
 *
 * Licensed 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 {toSize, toBuffer, toColor, toPoint, toRect,
    toRectF, toRegion, toTransform} from './common';
import intDefMapping from
    '../../../../../prebuilts/misc/common/winscope/intDefMapping.json';
import config from '../config/Configuration.json'

function readIntdefMap(): Map<string, string> {
    const map = new Map<string, string>();
    const keys = Object.keys(config.intDefColumn);

    keys.forEach(key => {
        const value = config.intDefColumn[key];
        map.set(key, value);
    });

    return map;
}
export default class ObjectFormatter {
    static displayDefaults: boolean = false
    private static INVALID_ELEMENT_PROPERTIES = config.invalidProperties;

    private static FLICKER_INTDEF_MAP = readIntdefMap();

    static cloneObject(entry: any): any {
        let obj: any = {}
        const properties = ObjectFormatter.getProperties(entry);
        properties.forEach(prop => obj[prop] = entry[prop]);
        return obj;
    }

    /**
     * Get the true properties of an entry excluding functions, kotlin gernerated
     * variables, explicitly excluded properties, and flicker objects already in
     * the hierarchy that shouldn't be traversed when formatting the entry
     * @param entry The entry for which we want to get the properties for
     * @return The "true" properties of the entry as described above
     */
    static getProperties(entry: any): string[] {
        var props = [];
        let obj = entry;

        do {
            const properties = Object.getOwnPropertyNames(obj).filter(it => {
                // filter out functions
                if (typeof(entry[it]) === 'function') return false;
                // internal propertires from kotlinJs
                if (it.includes(`$`)) return false;
                // private kotlin variables from kotlin
                if (it.startsWith(`_`)) return false;
                // some predefined properties used only internally (e.g., children, ref, diff)
                if (this.INVALID_ELEMENT_PROPERTIES.includes(it)) return false;

                const value = entry[it];
                // only non-empty arrays of non-flicker objects (otherwise they are in hierarchy)
                if (Array.isArray(value) && value.length > 0) return !value[0].stableId;
                // non-flicker object
                return !(value?.stableId);
            });
            properties.forEach(function (prop) {
                if (typeof(entry[prop]) !== 'function' && props.indexOf(prop) === -1) {
                    props.push(prop);
                }
            });
        } while (obj = Object.getPrototypeOf(obj));

        return props;
    }

    /**
     * Format a Winscope entry to be displayed in the UI
     * Accounts for different user display settings (e.g. hiding empty/default values)
     * @param obj The raw object to format
     * @return The formatted object
     */
    static format(obj: any): {} {
        const properties = this.getProperties(obj);
        const sortedProperties = properties.sort()

        const result: any = {}
        sortedProperties.forEach(entry => {
            const key = entry;
            const value: any = obj[key];

            if (value === null || value === undefined) {
                if (this.displayDefaults) {
                    result[key] = value
                }
                return
            }

            if (value || this.displayDefaults) {
                // flicker obj
                if (value.prettyPrint) {
                    const isEmpty = value.isEmpty === true;
                    if (!isEmpty || this.displayDefaults) {
                        result[key] = value.prettyPrint()
                    }
                } else {
                    // converted proto to flicker
                    const translatedObject = this.translateObject(value)
                    if (translatedObject) {
                        result[key] = translatedObject.prettyPrint()
                    // objects - recursive call
                    } else if (value && typeof(value) == `object`) {
                        const childObj = this.format(value) as any
                        const isEmpty = Object.entries(childObj).length == 0 || childObj.isEmpty
                        if (!isEmpty || this.displayDefaults) {
                            result[key] = childObj
                        }
                    } else {
                    // values
                        result[key] = this.translateIntDef(obj, key, value)
                    }
                }

            }
        })

        // return Object.freeze(result)
        return result
    }

    /**
     * Translate some predetermined proto objects into their flicker equivalent
     *
     * Returns null if the object cannot be translated
     *
     * @param obj Object to translate
     */
    private static translateObject(obj) {
        const type = obj?.$type?.name
        switch(type) {
            case `SizeProto`: return toSize(obj)
            case `ActiveBufferProto`: return toBuffer(obj)
            case `ColorProto`: return toColor(obj)
            case `PointProto`: return toPoint(obj)
            case `RectProto`: return toRect(obj)
            case `FloatRectProto`: return toRectF(obj)
            case `RegionProto`: return toRegion(obj)
            case `TransformProto`: return toTransform(obj)
            case 'ColorTransformProto': {
                const formatted = this.formatColorTransform(obj.val);
                return `${formatted}`;
            }
        }

        return null
    }

    private static formatColorTransform(vals) {
        const fixedVals = vals.map((v) => v.toFixed(1));
        let formatted = ``;
        for (let i = 0; i < fixedVals.length; i += 4) {
            formatted += `[`;
            formatted += fixedVals.slice(i, i + 4).join(', ');
            formatted += `] `;
        }
        return formatted;
    }

    /**
     * Obtains from the proto field, the metadata related to the typedef type (if any)
     *
     * @param obj Proto object
     * @param propertyName Property to search
     */
    private static getTypeDefSpec(obj: any, propertyName: string): string {
        const fields = obj?.$type?.fields
        if (!fields) {
            return null
        }

        const options = fields[propertyName]?.options
        if (!options) {
            return null
        }

        return options["(.android.typedef)"]
    }

    /**
     * Translate intdef properties into their string representation
     *
     * For proto objects check the
     *
     * @param parentObj Object containing the value to parse
     * @param propertyName Property to search
     * @param value Property value
     */
    private static translateIntDef(parentObj: any, propertyName: string, value: any): string {
        const parentClassName = parentObj.constructor.name
        const propertyPath = `${parentClassName}.${propertyName}`

        let translatedValue = value
        // Parse Flicker objects (no intdef annotation supported)
        if (this.FLICKER_INTDEF_MAP.has(propertyPath)) {
            translatedValue = this.getIntFlagsAsStrings(value,
                this.FLICKER_INTDEF_MAP.get(propertyPath))
        } else {
            // If it's a proto, search on the proto definition for the intdef type
            const typeDefSpec = this.getTypeDefSpec(parentObj, propertyName)
            if (typeDefSpec) {
                translatedValue = this.getIntFlagsAsStrings(value, typeDefSpec)
            }
        }

        return translatedValue
    }

    /**
     * Translate a property from its numerical value into its string representation
     *
     * @param intFlags Property value
     * @param annotationType IntDef type to use
     */
    private static getIntFlagsAsStrings(intFlags: any, annotationType: string) {
        const flags = [];

        const mapping = intDefMapping[annotationType].values;
        const knownFlagValues = Object.keys(mapping).reverse().map(x => parseInt(x));

        if (mapping.length == 0) {
            console.warn("No mapping for type", annotationType)
            return intFlags + ""
        }

        // Will only contain bits that have not been associated with a flag.
        const parsedIntFlags = parseInt(intFlags);
        let leftOver = parsedIntFlags;

        for (const flagValue of knownFlagValues) {
          if (((leftOver & flagValue) && ((intFlags & flagValue) === flagValue))
                || (parsedIntFlags === 0 && flagValue === 0)) {
            flags.push(mapping[flagValue]);

            leftOver = leftOver & ~flagValue;
          }
        }

        if (flags.length === 0) {
          console.error('No valid flag mappings found for ',
              intFlags, 'of type', annotationType);
        }

        if (leftOver) {
          // If 0 is a valid flag value that isn't in the intDefMapping
          // it will be ignored
          flags.push(leftOver);
        }

        return flags.join(' | ');
      }
}
