// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

//  Copyright (C) 2012 Google Inc. All rights reserved.

//  Redistribution and use in source and binary forms, with or without
//  modification, are permitted provided that the following conditions
//  are met:

//  1.  Redistributions of source code must retain the above copyright
//      notice, this list of conditions and the following disclaimer.
//  2.  Redistributions in binary form must reproduce the above copyright
//      notice, this list of conditions and the following disclaimer in the
//      documentation and/or other materials provided with the distribution.
//  3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
//      its contributors may be used to endorse or promote products derived
//      from this software without specific prior written permission.

//  THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
//  EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
//  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
//  DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
//  DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
//  (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
//  LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
//  ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
//  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
//  THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


import {AreaBounds, Bounds} from './common.js';
import {drawGridLabels, GridLabelState} from './css_grid_label_helpers.js';
import {applyMatrixToPoint, buildPath, emptyBounds, hatchFillPath} from './highlight_common.js';

const DEFAULT_EXTENDED_LINE_COLOR = 'rgba(128, 128, 128, 0.3)';

// TODO(alexrudenko): Grid label unit tests depend on this style so it cannot be extracted yet.
export const gridStyle = `
/* Grid row and column labels */
.grid-label-content {
  position: absolute;
  -webkit-user-select: none;
  padding: 2px;
  font-family: Menlo, monospace;
  font-size: 10px;
  min-width: 17px;
  min-height: 15px;
  border-radius: 2px;
  box-sizing: border-box;
  z-index: 1;
  background-clip: padding-box;
  pointer-events: none;
  text-align: center;
  display: flex;
  justify-content: center;
  align-items: center;
}

.grid-label-content[data-direction=row] {
  background-color: var(--row-label-color, #1A73E8);
  color: var(--row-label-text-color, #121212);
}

.grid-label-content[data-direction=column] {
  background-color: var(--column-label-color, #1A73E8);
  color: var(--column-label-text-color,#121212);
}

.line-names ul,
.line-names .line-name {
  margin: 0;
  padding: 0;
  list-style: none;
}

.line-names .line-name {
  max-width: 100px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.line-names .grid-label-content,
.line-numbers .grid-label-content,
.track-sizes .grid-label-content {
  border: 1px solid white;
  --inner-corner-avoid-distance: 15px;
}

.grid-label-content.top-left.inner-shared-corner,
.grid-label-content.top-right.inner-shared-corner {
  transform: translateY(var(--inner-corner-avoid-distance));
}

.grid-label-content.bottom-left.inner-shared-corner,
.grid-label-content.bottom-right.inner-shared-corner {
  transform: translateY(calc(var(--inner-corner-avoid-distance) * -1));
}

.grid-label-content.left-top.inner-shared-corner,
.grid-label-content.left-bottom.inner-shared-corner {
  transform: translateX(var(--inner-corner-avoid-distance));
}

.grid-label-content.right-top.inner-shared-corner,
.grid-label-content.right-bottom.inner-shared-corner {
  transform: translateX(calc(var(--inner-corner-avoid-distance) * -1));
}

.line-names .grid-label-content::before,
.line-numbers .grid-label-content::before,
.track-sizes .grid-label-content::before {
  position: absolute;
  z-index: 1;
  pointer-events: none;
  content: "";
  width: 3px;
  height: 3px;
  border: 1px solid white;
  border-width: 0 1px 1px 0;
}

.line-names .grid-label-content[data-direction=row]::before,
.line-numbers .grid-label-content[data-direction=row]::before,
.track-sizes .grid-label-content[data-direction=row]::before {
  background: var(--row-label-color, #1A73E8);
}

.line-names .grid-label-content[data-direction=column]::before,
.line-numbers .grid-label-content[data-direction=column]::before,
.track-sizes .grid-label-content[data-direction=column]::before {
  background: var(--column-label-color, #1A73E8);
}

.grid-label-content.bottom-mid::before {
  transform: translateY(-1px) rotate(45deg);
  top: 100%;
}

.grid-label-content.top-mid::before {
  transform: translateY(-3px) rotate(-135deg);
  top: 0%;
}

.grid-label-content.left-mid::before {
  transform: translateX(-3px) rotate(135deg);
  left: 0%
}

.grid-label-content.right-mid::before {
  transform: translateX(3px) rotate(-45deg);
  right: 0%;
}

.grid-label-content.right-top::before {
  transform: translateX(3px) translateY(-1px) rotate(-90deg) skewY(30deg);
  right: 0%;
  top: 0%;
}

.grid-label-content.right-bottom::before {
  transform: translateX(3px) translateY(-3px) skewX(30deg);
  right: 0%;
  top: 100%;
}

.grid-label-content.bottom-right::before {
  transform:  translateX(1px) translateY(-1px) skewY(30deg);
  right: 0%;
  top: 100%;
}

.grid-label-content.bottom-left::before {
  transform:  translateX(-1px) translateY(-1px) rotate(90deg) skewX(30deg);
  left: 0%;
  top: 100%;
}

.grid-label-content.left-top::before {
  transform: translateX(-3px) translateY(-1px) rotate(180deg) skewX(30deg);
  left: 0%;
  top: 0%;
}

.grid-label-content.left-bottom::before {
  transform: translateX(-3px) translateY(-3px) rotate(90deg) skewY(30deg);
  left: 0%;
  top: 100%;
}

.grid-label-content.top-right::before {
  transform:  translateX(1px) translateY(-3px) rotate(-90deg) skewX(30deg);
  right: 0%;
  top: 0%;
}

.grid-label-content.top-left::before {
  transform:  translateX(-1px) translateY(-3px) rotate(180deg) skewY(30deg);
  left: 0%;
  top: 0%;
}

@media (forced-colors: active) {
  .grid-label-content {
      border-color: Highlight;
      background-color: Canvas;
      color: Text;
      forced-color-adjust: none;
  }
  .grid-label-content::before {
    background-color: Canvas;
    border-color: Highlight;
  }
}`;

export interface GridHighlight {
  gridBorder: Array<string|number>;
  writingMode: string;
  rowGaps: Array<string|number>;
  rotationAngle: number;
  columnGaps: Array<string|number>;
  rows: Array<string|number>;
  columns: Array<string|number>;
  areaNames: {[key: string]: Array<string|number>};
  gridHighlightConfig: {
    gridBackgroundColor?: string,
    gridBorderColor?: string,
    rowGapColor?: string,
    columnGapColor?: string,
    areaBorderColor?: string,
                   gridBorderDash: boolean,
                   rowLineDash: boolean,
                   columnLineDash: boolean,
                   showGridExtensionLines: boolean,
                   showPositiveLineNumbers: boolean,
                   showNegativeLineNumbers: boolean,
                   rowLineColor: string,
                   columnLineColor: string,
                   rowHatchColor: string,
                   columnHatchColor: string,
                   showLineNames: boolean,
  };
}

export function drawLayoutGridHighlight(
    highlight: GridHighlight, context: CanvasRenderingContext2D, deviceScaleFactor: number, canvasWidth: number,
    canvasHeight: number, emulationScaleFactor: number, labelState: GridLabelState) {
  const gridBounds = emptyBounds();
  const gridPath = buildPath(highlight.gridBorder, gridBounds, emulationScaleFactor);

  // Transform the context to match the current writing-mode.
  context.save();
  applyWritingModeTransformation(highlight.writingMode, gridBounds, context);

  // Draw grid background
  if (highlight.gridHighlightConfig.gridBackgroundColor) {
    context.fillStyle = highlight.gridHighlightConfig.gridBackgroundColor;
    context.fill(gridPath);
  }

  // Draw Grid border
  if (highlight.gridHighlightConfig.gridBorderColor) {
    context.save();
    context.translate(0.5, 0.5);
    context.lineWidth = 0;
    if (highlight.gridHighlightConfig.gridBorderDash) {
      context.setLineDash([3, 3]);
    }
    context.strokeStyle = highlight.gridHighlightConfig.gridBorderColor;
    context.stroke(gridPath);
    context.restore();
  }

  // Draw grid lines
  const rowBounds = drawGridLines(context, highlight, 'row', emulationScaleFactor);
  const columnBounds = drawGridLines(context, highlight, 'column', emulationScaleFactor);

  // Draw gaps
  drawGridGap(
      context, highlight.rowGaps, highlight.gridHighlightConfig.rowGapColor,
      highlight.gridHighlightConfig.rowHatchColor, highlight.rotationAngle, emulationScaleFactor,
      /* flipDirection */ true);
  drawGridGap(
      context, highlight.columnGaps, highlight.gridHighlightConfig.columnGapColor,
      highlight.gridHighlightConfig.columnHatchColor, highlight.rotationAngle, emulationScaleFactor,
      /* flipDirection */ false);

  // Draw named grid areas
  const areaBounds =
      drawGridAreas(context, highlight.areaNames, highlight.gridHighlightConfig.areaBorderColor, emulationScaleFactor);

  // The rest of the overlay is drawn without the writing-mode transformation, but we keep the matrix to transform relevant points.
  const writingModeMatrix = context.getTransform();
  writingModeMatrix.scaleSelf(1 / deviceScaleFactor);
  context.restore();

  if (highlight.gridHighlightConfig.showGridExtensionLines) {
    if (rowBounds) {
      drawExtendedGridLines(
          context, rowBounds, highlight.gridHighlightConfig.rowLineDash, writingModeMatrix, canvasWidth, canvasHeight);
    }
    if (columnBounds) {
      drawExtendedGridLines(
          context, columnBounds, highlight.gridHighlightConfig.columnLineDash, writingModeMatrix, canvasWidth,
          canvasHeight);
    }
  }

  // Draw all the labels
  drawGridLabels(
      highlight, gridBounds, areaBounds, {canvasWidth, canvasHeight}, labelState, emulationScaleFactor,
      writingModeMatrix);
}

function applyWritingModeTransformation(writingMode: string, gridBounds: Bounds, context: CanvasRenderingContext2D) {
  if (writingMode !== 'vertical-rl' && writingMode !== 'vertical-lr') {
    return;
  }

  const topLeft = gridBounds.allPoints[0];
  const bottomLeft = gridBounds.allPoints[3];

  // Move to the top-left corner to do all transformations there.
  context.translate(topLeft.x, topLeft.y);

  if (writingMode === 'vertical-rl') {
    context.rotate(90 * Math.PI / 180);
    context.translate(0, -1 * (bottomLeft.y - topLeft.y));
  }

  if (writingMode === 'vertical-lr') {
    context.rotate(90 * Math.PI / 180);
    context.scale(1, -1);
  }

  // Move back to the original point.
  context.translate(topLeft.x * -1, topLeft.y * -1);
}

function drawGridLines(
    context: CanvasRenderingContext2D, highlight: GridHighlight, direction: 'row'|'column',
    emulationScaleFactor: number) {
  const tracks = highlight[`${direction}s` as 'rows' | 'columns'];
  const color = highlight.gridHighlightConfig[`${direction}LineColor` as 'rowLineColor' | 'columnLineColor'];
  const dash = highlight.gridHighlightConfig[`${direction}LineDash` as 'rowLineDash' | 'columnLineDash'];

  if (!color) {
    return null;
  }

  const bounds = emptyBounds();
  const path = buildPath(tracks, bounds, emulationScaleFactor);

  context.save();
  context.translate(0.5, 0.5);
  if (dash) {
    context.setLineDash([3, 3]);
  }
  context.lineWidth = 0;
  context.strokeStyle = color;

  context.save();
  context.stroke(path);
  context.restore();

  context.restore();

  return bounds;
}

function drawExtendedGridLines(
    context: CanvasRenderingContext2D, bounds: Bounds, dash: boolean|undefined, writingModeMatrix: DOMMatrix,
    canvasWidth: number, canvasHeight: number) {
  context.save();
  context.strokeStyle = DEFAULT_EXTENDED_LINE_COLOR;
  context.lineWidth = 1;
  context.translate(0.5, 0.5);
  if (dash) {
    context.setLineDash([3, 3]);
  }

  // A grid track path is a list of lines defined by 2 points.
  // Here we're going through the list of all points 2 by 2, so we can draw the extensions at the edges of each line.
  for (let i = 0; i < bounds.allPoints.length; i += 2) {
    let point1 = applyMatrixToPoint(bounds.allPoints[i], writingModeMatrix);
    let point2 = applyMatrixToPoint(bounds.allPoints[i + 1], writingModeMatrix);
    let edgePoint1;
    let edgePoint2;

    if (point1.x === point2.x) {
      // Special case for a vertical line.
      edgePoint1 = {x: point1.x, y: 0};
      edgePoint2 = {x: point1.x, y: canvasHeight};
      if (point2.y < point1.y) {
        [point1, point2] = [point2, point1];
      }
    } else if (point1.y === point2.y) {
      // Special case for a horizontal line.
      edgePoint1 = {x: 0, y: point1.y};
      edgePoint2 = {x: canvasWidth, y: point1.y};
      if (point2.x < point1.x) {
        [point1, point2] = [point2, point1];
      }
    } else {
      // When the line isn't straight, we need to do some maths.
      const a = (point2.y - point1.y) / (point2.x - point1.x);
      const b = (point1.y * point2.x - point2.y * point1.x) / (point2.x - point1.x);

      edgePoint1 = {x: 0, y: b};
      edgePoint2 = {x: canvasWidth, y: (canvasWidth * a) + b};

      if (point2.x < point1.x) {
        [point1, point2] = [point2, point1];
      }
    }

    context.beginPath();
    context.moveTo(edgePoint1.x, edgePoint1.y);
    context.lineTo(point1.x, point1.y);
    context.moveTo(point2.x, point2.y);
    context.lineTo(edgePoint2.x, edgePoint2.y);
    context.stroke();
  }

  context.restore();
}

/**
 * Draw all of the named grid area paths. This does not draw the labels, as
 * placing labels in and around the grid for various things is handled later.
 */
function drawGridAreas(
    context: CanvasRenderingContext2D, areas: {[key: string]: Array<string|number>}, borderColor: string|undefined,
    emulationScaleFactor: number): AreaBounds[] {
  if (!areas || !Object.keys(areas).length) {
    return [];
  }

  context.save();
  if (borderColor) {
    context.strokeStyle = borderColor;
  }
  context.lineWidth = 2;

  const areaBounds = [];

  for (const name in areas) {
    const areaCommands = areas[name];

    const bounds = emptyBounds();
    const path = buildPath(areaCommands, bounds, emulationScaleFactor);

    context.stroke(path);

    areaBounds.push({name, bounds});
  }

  context.restore();

  return areaBounds;
}

function drawGridGap(
    context: CanvasRenderingContext2D, gapCommands: Array<number|string>, gapColor: string|undefined,
    hatchColor: string|undefined, rotationAngle: number, emulationScaleFactor: number,
    flipDirection: boolean|undefined) {
  if (!gapColor && !hatchColor) {
    return;
  }

  context.save();
  context.translate(0.5, 0.5);
  context.lineWidth = 0;

  const bounds = emptyBounds();
  const path = buildPath(gapCommands, bounds, emulationScaleFactor);

  // Fill the gap background if needed.
  if (gapColor) {
    context.fillStyle = gapColor;
    context.fill(path);
  }

  // And draw the hatch pattern if needed.
  if (hatchColor) {
    hatchFillPath(context, path, bounds, /* delta */ 10, hatchColor, rotationAngle, flipDirection);
  }
  context.restore();
}
