/*
 * Copyright 2000-2014 Vaadin Ltd.
 * 
 * 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.
 */
package com.vaadin.client;

import com.google.gwt.core.client.JsArrayString;
import com.google.gwt.dom.client.Element;

public class MeasuredSize {
    private final static boolean debugSizeChanges = false;

    public static class MeasureResult {
        private final boolean widthChanged;
        private final boolean heightChanged;

        private MeasureResult(boolean widthChanged, boolean heightChanged) {
            this.widthChanged = widthChanged;
            this.heightChanged = heightChanged;
        }

        public boolean isHeightChanged() {
            return heightChanged;
        }

        public boolean isWidthChanged() {
            return widthChanged;
        }

        public boolean isChanged() {
            return heightChanged || widthChanged;
        }
    }

    private int width = -1;
    private int height = -1;

    private int[] paddings = new int[4];
    private int[] borders = new int[4];
    private int[] margins = new int[4];

    private FastStringSet dependents = FastStringSet.create();

    public int getOuterHeight() {
        return height;
    }

    public int getOuterWidth() {
        return width;
    }

    public void addDependent(String pid) {
        dependents.add(pid);
    }

    public void removeDependent(String pid) {
        dependents.remove(pid);
    }

    public boolean hasDependents() {
        return !dependents.isEmpty();
    }

    public JsArrayString getDependents() {
        return dependents.dump();
    }

    private static int sumWidths(int[] sizes) {
        return sizes[1] + sizes[3];
    }

    private static int sumHeights(int[] sizes) {
        return sizes[0] + sizes[2];
    }

    public int getInnerHeight() {
        return height - sumHeights(margins) - sumHeights(borders)
                - sumHeights(paddings);
    }

    public int getInnerWidth() {
        return width - sumWidths(margins) - sumWidths(borders)
                - sumWidths(paddings);
    }

    public boolean setOuterHeight(int height) {
        if (this.height != height) {
            this.height = height;
            return true;
        } else {
            return false;
        }
    }

    public boolean setOuterWidth(int width) {
        if (this.width != width) {
            this.width = width;
            return true;
        } else {
            return false;
        }
    }

    public int getBorderHeight() {
        return sumHeights(borders);
    }

    public int getBorderWidth() {
        return sumWidths(borders);
    }

    public int getPaddingHeight() {
        return sumHeights(paddings);
    }

    public int getPaddingWidth() {
        return sumWidths(paddings);
    }

    public int getMarginHeight() {
        return sumHeights(margins);
    }

    public int getMarginWidth() {
        return sumWidths(margins);
    }

    public int getMarginTop() {
        return margins[0];
    }

    public int getMarginRight() {
        return margins[1];
    }

    public int getMarginBottom() {
        return margins[2];
    }

    public int getMarginLeft() {
        return margins[3];
    }

    public int getBorderTop() {
        return borders[0];
    }

    public int getBorderRight() {
        return borders[1];
    }

    public int getBorderBottom() {
        return borders[2];
    }

    public int getBorderLeft() {
        return borders[3];
    }

    public int getPaddingTop() {
        return paddings[0];
    }

    public int getPaddingRight() {
        return paddings[1];
    }

    public int getPaddingBottom() {
        return paddings[2];
    }

    public int getPaddingLeft() {
        return paddings[3];
    }

    public MeasureResult measure(Element element) {
        Profiler.enter("MeasuredSize.measure");
        boolean heightChanged = false;
        boolean widthChanged = false;

        Profiler.enter("new ComputedStyle");
        ComputedStyle computedStyle = new ComputedStyle(element);
        int[] paddings = computedStyle.getPadding();
        // Some browsers do not reflow until accessing data from the computed
        // style object
        Profiler.leave("new ComputedStyle");

        Profiler.enter("Measure paddings");
        if (!heightChanged && hasHeightChanged(this.paddings, paddings)) {
            debugSizeChange(element, "Height (padding)", this.paddings,
                    paddings);
            heightChanged = true;
        }
        if (!widthChanged && hasWidthChanged(this.paddings, paddings)) {
            debugSizeChange(element, "Width (padding)", this.paddings, paddings);
            widthChanged = true;
        }
        this.paddings = paddings;
        Profiler.leave("Measure paddings");

        Profiler.enter("Measure margins");
        int[] margins = computedStyle.getMargin();
        if (!heightChanged && hasHeightChanged(this.margins, margins)) {
            debugSizeChange(element, "Height (margins)", this.margins, margins);
            heightChanged = true;
        }
        if (!widthChanged && hasWidthChanged(this.margins, margins)) {
            debugSizeChange(element, "Width (margins)", this.margins, margins);
            widthChanged = true;
        }
        this.margins = margins;
        Profiler.leave("Measure margins");

        Profiler.enter("Measure borders");
        int[] borders = computedStyle.getBorder();
        if (!heightChanged && hasHeightChanged(this.borders, borders)) {
            debugSizeChange(element, "Height (borders)", this.borders, borders);
            heightChanged = true;
        }
        if (!widthChanged && hasWidthChanged(this.borders, borders)) {
            debugSizeChange(element, "Width (borders)", this.borders, borders);
            widthChanged = true;
        }
        this.borders = borders;
        Profiler.leave("Measure borders");

        Profiler.enter("Measure height");
        int requiredHeight = Util.getRequiredHeight(element);
        int marginHeight = sumHeights(margins);
        int oldHeight = height;
        int oldWidth = width;
        if (setOuterHeight(requiredHeight + marginHeight)) {
            debugSizeChange(element, "Height (outer)", oldHeight, height);
            heightChanged = true;
        }
        Profiler.leave("Measure height");

        Profiler.enter("Measure width");
        int requiredWidth = Util.getRequiredWidth(element);
        int marginWidth = sumWidths(margins);
        if (setOuterWidth(requiredWidth + marginWidth)) {
            debugSizeChange(element, "Width (outer)", oldWidth, width);
            widthChanged = true;
        }
        Profiler.leave("Measure width");

        Profiler.leave("MeasuredSize.measure");

        return new MeasureResult(widthChanged, heightChanged);
    }

    private void debugSizeChange(Element element, String sizeChangeType,
            int[] changedFrom, int[] changedTo) {
        debugSizeChange(element, sizeChangeType,
                java.util.Arrays.asList(changedFrom).toString(),
                java.util.Arrays.asList(changedTo).toString());
    }

    private void debugSizeChange(Element element, String sizeChangeType,
            int changedFrom, int changedTo) {
        debugSizeChange(element, sizeChangeType, String.valueOf(changedFrom),
                String.valueOf(changedTo));
    }

    private void debugSizeChange(Element element, String sizeChangeType,
            String changedFrom, String changedTo) {
        if (debugSizeChanges) {
            VConsole.log(sizeChangeType + " has changed from " + changedFrom
                    + " to " + changedTo + " for " + element.toString());
        }
    }

    private static boolean hasWidthChanged(int[] sizes1, int[] sizes2) {
        return sizes1[1] != sizes2[1] || sizes1[3] != sizes2[3];
    }

    private static boolean hasHeightChanged(int[] sizes1, int[] sizes2) {
        return sizes1[0] != sizes2[0] || sizes1[2] != sizes2[2];
    }

}
