/*
 * Copyright 2017, 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 {transform, nanos_to_string, get_visible_chip} from './transform.js'

// Layer flags
const FLAG_HIDDEN = 0x01;
const FLAG_OPAQUE = 0x02;
const FLAG_SECURE = 0x80;

var RELATIVE_Z_CHIP = {short: 'RelZ',
    long: "Is relative Z-ordered to another surface",
    class: 'warn'};
var RELATIVE_Z_PARENT_CHIP = {short: 'RelZParent',
    long: "Something is relative Z-ordered to this surface",
    class: 'warn'};
var MISSING_LAYER = {short: 'MissingLayer',
    long: "This layer was referenced from the parent, but not present in the trace",
    class: 'error'};

function transform_layer(layer, {parentBounds, parentHidden}) {
  function get_size(layer) {
    var size = layer.size || {w: 0, h: 0};
    return {
      left: 0,
      right: size.w,
      top: 0,
      bottom: size.h
    };
  }

  function get_crop(layer) {
    var crop = layer.crop || {left: 0, top: 0, right: 0 , bottom:0};
    return {
      left: crop.left || 0,
      right: crop.right  || 0,
      top: crop.top || 0,
      bottom: crop.bottom || 0
    };
  }

  function intersect(bounds, crop) {
    return {
      left: Math.max(crop.left, bounds.left),
      right: Math.min(crop.right, bounds.right),
      top: Math.max(crop.top, bounds.top),
      bottom: Math.min(crop.bottom, bounds.bottom),
    };
  }

  function is_empty_rect(rect) {
    var right = rect.right || 0;
    var left = rect.left || 0;
    var top = rect.top || 0;
    var bottom = rect.bottom || 0;

    return (right - left) <= 0 || (bottom - top) <= 0;
  }

  function get_cropped_bounds(layer, parentBounds) {
    var size = get_size(layer);
    var crop = get_crop(layer);
    if (!is_empty_rect(size) && !is_empty_rect(crop)) {
      return intersect(size, crop);
    }
    if (!is_empty_rect(size)) {
      return size;
    }
    if (!is_empty_rect(crop)) {
      return crop;
    }
    return parentBounds || { left: 0, right: 0, top: 0, bottom: 0 };
  }

  function offset_to(bounds, x, y) {
    return {
      right: bounds.right - (bounds.left - x),
      bottom: bounds.bottom - (bounds.top - y),
      left: x,
      top: y,
    };
  }

  function transform_bounds(layer, parentBounds) {
    var result = layer.bounds || get_cropped_bounds(layer, parentBounds);
    var tx = (layer.position) ? layer.position.x || 0 : 0;
    var ty = (layer.position) ? layer.position.y || 0 : 0;
    result = offset_to(result, 0, 0);
    result.label = layer.name;
    result.transform = layer.transform || {dsdx:1, dtdx:0, dsdy:0, dtdy:1};
    result.transform.tx = tx;
    result.transform.ty = ty;
    return result;
  }

  function is_opaque(layer) {
    return layer.color == undefined || (layer.color.a || 0) > 0;
  }

  function is_empty(region) {
    return region == undefined ||
        region.rect == undefined ||
        region.rect.length == 0 ||
        region.rect.every(function(r) { return is_empty_rect(r) } );
  }

  function is_rect_empty_and_valid(rect) {
    return rect &&
      (rect.left - rect.right === 0 || rect.top - rect.bottom === 0);
  }

  function is_transform_invalid(transform) {
    return !transform || (transform.dsdx * transform.dtdy ===
        transform.dtdx * transform.dsdy); //determinant of transform
        /**
         * The transformation matrix is defined as the product of:
         * | cos(a) -sin(a) |  \/  | X 0 |
         * | sin(a)  cos(a) |  /\  | 0 Y |
         *
         * where a is a rotation angle, and X and Y are scaling factors.
         * A transformation matrix is invalid when either X or Y is zero,
         * as a rotation matrix is valid for any angle. When either X or Y
         * is 0, then the scaling matrix is not invertible, which makes the
         * transformation matrix not invertible as well. A 2D matrix with
         * components | A B | is uninvertible if and only if AD - BC = 0.
         *            | C D |
         * This check is included above.
         */
  }

  /**
   * Checks if the layer is visible on screen according to its type,
   * active buffer content, alpha and visible regions.
   *
   * @param {layer} layer
   * @returns if the layer is visible on screen or not
   */
  function is_visible(layer) {
    var visible = (layer.activeBuffer || layer.type === 'ColorLayer')
                  && !hidden && is_opaque(layer);
    visible &= !is_empty(layer.visibleRegion);
    return visible;
  }

  function postprocess_flags(layer) {
    if (!layer.flags) return;
    var verboseFlags = [];
    if (layer.flags & FLAG_HIDDEN) {
      verboseFlags.push("HIDDEN");
    }
    if (layer.flags & FLAG_OPAQUE) {
      verboseFlags.push("OPAQUE");
    }
    if (layer.flags & FLAG_SECURE) {
      verboseFlags.push("SECURE");
    }

    layer.flags = verboseFlags.join('|') + " (" + layer.flags + ")";
  }

  var chips = [];
  var rect = transform_bounds(layer, parentBounds);
  var hidden = (layer.flags & FLAG_HIDDEN) != 0 || parentHidden;
  var visible = is_visible(layer);
  if (visible) {
    chips.push(get_visible_chip());
  } else {
    rect = undefined;
  }

  var bounds = undefined;
  if (layer.name.startsWith("Display Root#0") && layer.sourceBounds) {
    bounds = {width: layer.sourceBounds.right, height: layer.sourceBounds.bottom};
  }

  if ((layer.zOrderRelativeOf || -1) !== -1) {
    chips.push(RELATIVE_Z_CHIP);
  }
  if (layer.zOrderRelativeParentOf !== undefined) {
    chips.push(RELATIVE_Z_PARENT_CHIP);
  }
  if (layer.missing) {
    chips.push(MISSING_LAYER);
  }
  function visibilityReason(layer) {
    var reasons = [];
    if (!layer.color || layer.color.a === 0) {
      reasons.push('Alpha is 0');
    }
    if (layer.flags && (layer.flags & FLAG_HIDDEN != 0)) {
      reasons.push('Flag is hidden');
    }
    if (is_rect_empty_and_valid(layer.crop)) {
      reasons.push('Crop is zero');
    }
    if (is_transform_invalid(layer.transform)) {
      reasons.push('Transform is invalid');
    }
    if (layer.isRelativeOf && layer.zOrderRelativeOf == -1) {
      reasons.push('RelativeOf layer has been removed');
    }
    return reasons.join();
  }
  if (parentHidden) {
    layer.invisibleDueTo = 'Hidden by parent with ID: ' + parentHidden;
  } else {
    let reasons_hidden = visibilityReason(layer);
    let isBufferLayer = (layer.type === 'BufferStateLayer' || layer.type === 'BufferQueueLayer');
    if (reasons_hidden) {
      layer.invisibleDueTo = reasons_hidden;
      parentHidden = layer.id
    } else if (layer.type === 'ContainerLayer') {
        layer.invisibleDueTo = 'This is a ContainerLayer.';
    } else if (isBufferLayer && (!layer.activeBuffer ||
          layer.activeBuffer.height === 0 || layer.activeBuffer.width === 0)) {
        layer.invisibleDueTo = 'The buffer is empty.';
    } else if (!visible) {
        layer.invisibleDueTo = 'Unknown. Occluded by another layer?';
    }
  }
  var transform_layer_with_parent_hidden =
      (layer) => transform_layer(layer, {parentBounds: rect, parentHidden: parentHidden});
  postprocess_flags(layer);
  return transform({
    obj: layer,
    kind: '',
    name: layer.id + ": " + layer.name,
    children: [
      [layer.resolvedChildren, transform_layer_with_parent_hidden],
    ],
    rect,
    bounds,
    highlight: rect,
    chips,
    visible,
  });
}

function missingLayer(childId) {
  return {
    name: "layer #" + childId,
    missing: true,
    zOrderRelativeOf: -1,
    transform: {dsdx:1, dtdx:0, dsdy:0, dtdy:1},
  }
}

function transform_layers(layers) {
  var idToItem = {};
  var isChild = {}

  var layersList = layers.layers || [];

  layersList.forEach((e) => {
    idToItem[e.id] = e;
  });
  layersList.forEach((e) => {
    e.resolvedChildren = [];
    if (Array.isArray(e.children)) {
      e.resolvedChildren = e.children.map(
          (childId) => idToItem[childId] || missingLayer(childId));
      e.children.forEach((childId) => {
        isChild[childId] = true;
      });
    }
    if ((e.zOrderRelativeOf || -1) !== -1) {
      idToItem[e.zOrderRelativeOf].zOrderRelativeParentOf = e.id;
    }
  });

  var roots = layersList.filter((e) => !isChild[e.id]);

  function foreachTree(nodes, fun) {
    nodes.forEach((n) => {
      fun(n);
      foreachTree(n.children, fun);
    });
  }

  var idToTransformed = {};
  var transformed_roots = roots.map((r) =>
    transform_layer(r, {parentBounds: {left: 0, right: 0, top: 0, bottom: 0},
      parentHidden: null}));

  foreachTree(transformed_roots, (n) => {
    idToTransformed[n.obj.id] = n;
  });
  var flattened = [];
  layersList.forEach((e) => {
    flattened.push(idToTransformed[e.id]);
  });

  return transform({
    obj: {},
    kind: 'layers',
    name: 'layers',
    children: [
      [transformed_roots, (c) => c],
    ],
    rects_transform (r) {
      var res = [];
      flattened.forEach((l) => {
        if (l.rect) {
          res.push(l.rect);
        }
      });
      return res.reverse();
    },
    flattened,
  });
}

function transform_layers_entry(entry) {
  return transform({
    obj: entry,
    kind: 'entry',
    name: nanos_to_string(entry.elapsedRealtimeNanos) + " - " + entry.where,
    children: [
      [[entry.layers], transform_layers],
    ],
    timestamp: entry.elapsedRealtimeNanos,
    stableId: 'entry',
  });
}

function transform_layers_trace(entries) {
  var r = transform({
    obj: entries,
    kind: 'layerstrace',
    name: 'layerstrace',
    children: [
      [entries.entry, transform_layers_entry],
    ],
  });

  return r;
}

export {transform_layers, transform_layers_trace};
