/*
* 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.
*/

// Layout helpers for each component positioning

import * as zrUtil from 'zrender/src/core/util';
import BoundingRect from 'zrender/src/core/BoundingRect';
import {parsePercent} from './number';
import * as formatUtil from './format';
import { BoxLayoutOptionMixin, ComponentLayoutMode } from './types';
import Group from 'zrender/src/graphic/Group';
import Element from 'zrender/src/Element';
import { Dictionary } from 'zrender/src/core/types';

const each = zrUtil.each;

export interface LayoutRect extends BoundingRect {
    margin: number[]
}

export interface NewlineElement extends Element {
    newline: boolean
}

type BoxLayoutKeys = keyof BoxLayoutOptionMixin;
/**
 * @public
 */
export const LOCATION_PARAMS = [
    'left', 'right', 'top', 'bottom', 'width', 'height'
] as const;

/**
 * @public
 */
export const HV_NAMES = [
    ['width', 'left', 'right'],
    ['height', 'top', 'bottom']
] as const;

function boxLayout(
    orient: 'horizontal' | 'vertical',
    group: Group,
    gap: number,
    maxWidth?: number,
    maxHeight?: number
) {
    let x = 0;
    let y = 0;

    if (maxWidth == null) {
        maxWidth = Infinity;
    }
    if (maxHeight == null) {
        maxHeight = Infinity;
    }
    let currentLineMaxSize = 0;

    group.eachChild(function (child, idx) {
        const rect = child.getBoundingRect();
        const nextChild = group.childAt(idx + 1);
        const nextChildRect = nextChild && nextChild.getBoundingRect();
        let nextX: number;
        let nextY: number;

        if (orient === 'horizontal') {
            const moveX = rect.width + (nextChildRect ? (-nextChildRect.x + rect.x) : 0);
            nextX = x + moveX;
            // Wrap when width exceeds maxWidth or meet a `newline` group
            // FIXME compare before adding gap?
            if (nextX > maxWidth || (child as NewlineElement).newline) {
                x = 0;
                nextX = moveX;
                y += currentLineMaxSize + gap;
                currentLineMaxSize = rect.height;
            }
            else {
                // FIXME: consider rect.y is not `0`?
                currentLineMaxSize = Math.max(currentLineMaxSize, rect.height);
            }
        }
        else {
            const moveY = rect.height + (nextChildRect ? (-nextChildRect.y + rect.y) : 0);
            nextY = y + moveY;
            // Wrap when width exceeds maxHeight or meet a `newline` group
            if (nextY > maxHeight || (child as NewlineElement).newline) {
                x += currentLineMaxSize + gap;
                y = 0;
                nextY = moveY;
                currentLineMaxSize = rect.width;
            }
            else {
                currentLineMaxSize = Math.max(currentLineMaxSize, rect.width);
            }
        }

        if ((child as NewlineElement).newline) {
            return;
        }

        child.x = x;
        child.y = y;
        child.markRedraw();

        orient === 'horizontal'
            ? (x = nextX + gap)
            : (y = nextY + gap);
    });
}

/**
 * VBox or HBox layouting
 * @param {string} orient
 * @param {module:zrender/graphic/Group} group
 * @param {number} gap
 * @param {number} [width=Infinity]
 * @param {number} [height=Infinity]
 */
export const box = boxLayout;

/**
 * VBox layouting
 * @param {module:zrender/graphic/Group} group
 * @param {number} gap
 * @param {number} [width=Infinity]
 * @param {number} [height=Infinity]
 */
export const vbox = zrUtil.curry(boxLayout, 'vertical');

/**
 * HBox layouting
 * @param {module:zrender/graphic/Group} group
 * @param {number} gap
 * @param {number} [width=Infinity]
 * @param {number} [height=Infinity]
 */
export const hbox = zrUtil.curry(boxLayout, 'horizontal');

/**
 * If x or x2 is not specified or 'center' 'left' 'right',
 * the width would be as long as possible.
 * If y or y2 is not specified or 'middle' 'top' 'bottom',
 * the height would be as long as possible.
 */
export function getAvailableSize(
    positionInfo: {
        left?: number | string
        top?: number | string
        right?: number | string
        bottom?: number | string
    },
    containerRect: { width: number, height: number },
    margin?: number[] | number
) {
    const containerWidth = containerRect.width;
    const containerHeight = containerRect.height;

    let x = parsePercent(positionInfo.left, containerWidth);
    let y = parsePercent(positionInfo.top, containerHeight);
    let x2 = parsePercent(positionInfo.right, containerWidth);
    let y2 = parsePercent(positionInfo.bottom, containerHeight);

    (isNaN(x) || isNaN(parseFloat(positionInfo.left as string))) && (x = 0);
    (isNaN(x2) || isNaN(parseFloat(positionInfo.right as string))) && (x2 = containerWidth);
    (isNaN(y) || isNaN(parseFloat(positionInfo.top as string))) && (y = 0);
    (isNaN(y2) || isNaN(parseFloat(positionInfo.bottom as string))) && (y2 = containerHeight);

    margin = formatUtil.normalizeCssArray(margin || 0);

    return {
        width: Math.max(x2 - x - margin[1] - margin[3], 0),
        height: Math.max(y2 - y - margin[0] - margin[2], 0)
    };
}

/**
 * Parse position info.
 */
export function getLayoutRect(
    positionInfo: BoxLayoutOptionMixin & {
        aspect?: number // aspect is width / height
    },
    containerRect: {width: number, height: number},
    margin?: number | number[]
): LayoutRect {
    margin = formatUtil.normalizeCssArray(margin || 0);

    const containerWidth = containerRect.width;
    const containerHeight = containerRect.height;

    let left = parsePercent(positionInfo.left, containerWidth);
    let top = parsePercent(positionInfo.top, containerHeight);
    const right = parsePercent(positionInfo.right, containerWidth);
    const bottom = parsePercent(positionInfo.bottom, containerHeight);
    let width = parsePercent(positionInfo.width, containerWidth);
    let height = parsePercent(positionInfo.height, containerHeight);

    const verticalMargin = margin[2] + margin[0];
    const horizontalMargin = margin[1] + margin[3];
    const aspect = positionInfo.aspect;

    // If width is not specified, calculate width from left and right
    if (isNaN(width)) {
        width = containerWidth - right - horizontalMargin - left;
    }
    if (isNaN(height)) {
        height = containerHeight - bottom - verticalMargin - top;
    }

    if (aspect != null) {
        // If width and height are not given
        // 1. Graph should not exceeds the container
        // 2. Aspect must be keeped
        // 3. Graph should take the space as more as possible
        // FIXME
        // Margin is not considered, because there is no case that both
        // using margin and aspect so far.
        if (isNaN(width) && isNaN(height)) {
            if (aspect > containerWidth / containerHeight) {
                width = containerWidth * 0.8;
            }
            else {
                height = containerHeight * 0.8;
            }
        }

        // Calculate width or height with given aspect
        if (isNaN(width)) {
            width = aspect * height;
        }
        if (isNaN(height)) {
            height = width / aspect;
        }
    }

    // If left is not specified, calculate left from right and width
    if (isNaN(left)) {
        left = containerWidth - right - width - horizontalMargin;
    }
    if (isNaN(top)) {
        top = containerHeight - bottom - height - verticalMargin;
    }

    // Align left and top
    switch (positionInfo.left || positionInfo.right) {
        case 'center':
            left = containerWidth / 2 - width / 2 - margin[3];
            break;
        case 'right':
            left = containerWidth - width - horizontalMargin;
            break;
    }
    switch (positionInfo.top || positionInfo.bottom) {
        case 'middle':
        case 'center':
            top = containerHeight / 2 - height / 2 - margin[0];
            break;
        case 'bottom':
            top = containerHeight - height - verticalMargin;
            break;
    }
    // If something is wrong and left, top, width, height are calculated as NaN
    left = left || 0;
    top = top || 0;
    if (isNaN(width)) {
        // Width may be NaN if only one value is given except width
        width = containerWidth - horizontalMargin - left - (right || 0);
    }
    if (isNaN(height)) {
        // Height may be NaN if only one value is given except height
        height = containerHeight - verticalMargin - top - (bottom || 0);
    }

    const rect = new BoundingRect(left + margin[3], top + margin[0], width, height) as LayoutRect;
    rect.margin = margin;
    return rect;
}


/**
 * Position a zr element in viewport
 *  Group position is specified by either
 *  {left, top}, {right, bottom}
 *  If all properties exists, right and bottom will be igonred.
 *
 * Logic:
 *     1. Scale (against origin point in parent coord)
 *     2. Rotate (against origin point in parent coord)
 *     3. Translate (with el.position by this method)
 * So this method only fixes the last step 'Translate', which does not affect
 * scaling and rotating.
 *
 * If be called repeatedly with the same input el, the same result will be gotten.
 *
 * Return true if the layout happened.
 *
 * @param el Should have `getBoundingRect` method.
 * @param positionInfo
 * @param positionInfo.left
 * @param positionInfo.top
 * @param positionInfo.right
 * @param positionInfo.bottom
 * @param positionInfo.width Only for opt.boundingModel: 'raw'
 * @param positionInfo.height Only for opt.boundingModel: 'raw'
 * @param containerRect
 * @param margin
 * @param opt
 * @param opt.hv Only horizontal or only vertical. Default to be [1, 1]
 * @param opt.boundingMode
 *        Specify how to calculate boundingRect when locating.
 *        'all': Position the boundingRect that is transformed and uioned
 *               both itself and its descendants.
 *               This mode simplies confine the elements in the bounding
 *               of their container (e.g., using 'right: 0').
 *        'raw': Position the boundingRect that is not transformed and only itself.
 *               This mode is useful when you want a element can overflow its
 *               container. (Consider a rotated circle needs to be located in a corner.)
 *               In this mode positionInfo.width/height can only be number.
 */
export function positionElement(
    el: Element,
    positionInfo: BoxLayoutOptionMixin,
    containerRect: {width: number, height: number},
    margin?: number[] | number,
    opt?: {
        hv: [1 | 0 | boolean, 1 | 0 | boolean],
        boundingMode: 'all' | 'raw'
    },
    out?: { x?: number, y?: number }
): boolean {
    const h = !opt || !opt.hv || opt.hv[0];
    const v = !opt || !opt.hv || opt.hv[1];
    const boundingMode = opt && opt.boundingMode || 'all';
    out = out || el;

    out.x = el.x;
    out.y = el.y;

    if (!h && !v) {
        return false;
    }

    let rect;
    if (boundingMode === 'raw') {
        rect = el.type === 'group'
            ? new BoundingRect(0, 0, +positionInfo.width || 0, +positionInfo.height || 0)
            : el.getBoundingRect();
    }
    else {
        rect = el.getBoundingRect();
        if (el.needLocalTransform()) {
            const transform = el.getLocalTransform();
            // Notice: raw rect may be inner object of el,
            // which should not be modified.
            rect = rect.clone();
            rect.applyTransform(transform);
        }
    }

    // The real width and height can not be specified but calculated by the given el.
    const layoutRect = getLayoutRect(
        zrUtil.defaults(
            {width: rect.width, height: rect.height},
            positionInfo
        ),
        containerRect,
        margin
    );

    // Because 'tranlate' is the last step in transform
    // (see zrender/core/Transformable#getLocalTransform),
    // we can just only modify el.position to get final result.
    const dx = h ? layoutRect.x - rect.x : 0;
    const dy = v ? layoutRect.y - rect.y : 0;

    if (boundingMode === 'raw') {
        out.x = dx;
        out.y = dy;
    }
    else {
        out.x += dx;
        out.y += dy;
    }
    if (out === el) {
        el.markRedraw();
    }
    return true;
}

/**
 * @param option Contains some of the properties in HV_NAMES.
 * @param hvIdx 0: horizontal; 1: vertical.
 */
export function sizeCalculable(option: BoxLayoutOptionMixin, hvIdx: number): boolean {
    return option[HV_NAMES[hvIdx][0]] != null
        || (option[HV_NAMES[hvIdx][1]] != null && option[HV_NAMES[hvIdx][2]] != null);
}

export function fetchLayoutMode(ins: any): ComponentLayoutMode {
    const layoutMode = ins.layoutMode || ins.constructor.layoutMode;
    return zrUtil.isObject(layoutMode)
        ? layoutMode
        : layoutMode
        ? {type: layoutMode}
        : null;
}

/**
 * Consider Case:
 * When default option has {left: 0, width: 100}, and we set {right: 0}
 * through setOption or media query, using normal zrUtil.merge will cause
 * {right: 0} does not take effect.
 *
 * @example
 * ComponentModel.extend({
 *     init: function () {
 *         ...
 *         let inputPositionParams = layout.getLayoutParams(option);
 *         this.mergeOption(inputPositionParams);
 *     },
 *     mergeOption: function (newOption) {
 *         newOption && zrUtil.merge(thisOption, newOption, true);
 *         layout.mergeLayoutParam(thisOption, newOption);
 *     }
 * });
 *
 * @param targetOption
 * @param newOption
 * @param opt
 */
export function mergeLayoutParam<T extends BoxLayoutOptionMixin>(
    targetOption: T,
    newOption: T,
    opt?: ComponentLayoutMode
) {
    let ignoreSize = opt && opt.ignoreSize;
    !zrUtil.isArray(ignoreSize) && (ignoreSize = [ignoreSize, ignoreSize]);

    const hResult = merge(HV_NAMES[0], 0);
    const vResult = merge(HV_NAMES[1], 1);

    copy(HV_NAMES[0], targetOption, hResult);
    copy(HV_NAMES[1], targetOption, vResult);

    function merge(names: typeof HV_NAMES[number], hvIdx: number) {
        const newParams: BoxLayoutOptionMixin = {};
        let newValueCount = 0;
        const merged: BoxLayoutOptionMixin = {};
        let mergedValueCount = 0;
        const enoughParamNumber = 2;

        each(names, function (name: BoxLayoutKeys) {
            merged[name] = targetOption[name];
        });
        each(names, function (name: BoxLayoutKeys) {
            // Consider case: newOption.width is null, which is
            // set by user for removing width setting.
            hasProp(newOption, name) && (newParams[name] = merged[name] = newOption[name]);
            hasValue(newParams, name) && newValueCount++;
            hasValue(merged, name) && mergedValueCount++;
        });

        if ((ignoreSize as [boolean, boolean])[hvIdx]) {
            // Only one of left/right is premitted to exist.
            if (hasValue(newOption, names[1])) {
                merged[names[2]] = null;
            }
            else if (hasValue(newOption, names[2])) {
                merged[names[1]] = null;
            }
            return merged;
        }

        // Case: newOption: {width: ..., right: ...},
        // or targetOption: {right: ...} and newOption: {width: ...},
        // There is no conflict when merged only has params count
        // little than enoughParamNumber.
        if (mergedValueCount === enoughParamNumber || !newValueCount) {
            return merged;
        }
        // Case: newOption: {width: ..., right: ...},
        // Than we can make sure user only want those two, and ignore
        // all origin params in targetOption.
        else if (newValueCount >= enoughParamNumber) {
            return newParams;
        }
        else {
            // Chose another param from targetOption by priority.
            for (let i = 0; i < names.length; i++) {
                const name = names[i];
                if (!hasProp(newParams, name) && hasProp(targetOption, name)) {
                    newParams[name] = targetOption[name];
                    break;
                }
            }
            return newParams;
        }
    }

    function hasProp(obj: object, name: string): boolean {
        return obj.hasOwnProperty(name);
    }

    function hasValue(obj: Dictionary<any>, name: string): boolean {
        return obj[name] != null && obj[name] !== 'auto';
    }

    function copy(names: readonly string[], target: Dictionary<any>, source: Dictionary<any>) {
        each(names, function (name) {
            target[name] = source[name];
        });
    }
}

/**
 * Retrieve 'left', 'right', 'top', 'bottom', 'width', 'height' from object.
 */
export function getLayoutParams(source: BoxLayoutOptionMixin): BoxLayoutOptionMixin {
    return copyLayoutParams({}, source);
}

/**
 * Retrieve 'left', 'right', 'top', 'bottom', 'width', 'height' from object.
 * @param {Object} source
 * @return {Object} Result contains those props.
 */
export function copyLayoutParams(target: BoxLayoutOptionMixin, source: BoxLayoutOptionMixin): BoxLayoutOptionMixin {
    source && target && each(LOCATION_PARAMS, function (name: BoxLayoutKeys) {
        source.hasOwnProperty(name) && (target[name] = source[name]);
    });
    return target;
}
