// 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 {rgbaToHsla} from '../front_end/common/ColorUtils.js';
import {Bounds, Quad} from './common.js';

export type PathBounds = Bounds&{
  leftmostXForY: {[key: string]: number},
  rightmostXForY: {[key: string]: number},
  topmostYForX: {[key: string]: number},
  bottommostYForX: {[key: string]: number},
};

export interface LineStyle {
  color?: string;
  pattern?: LinePattern;
}

export interface BoxStyle {
  fillColor?: string;
  hatchColor?: string;
}

const enum LinePattern {
  Solid = 'solid',
  Dotted = 'dotted',
  Dashed = 'dashed',
}

export function drawPathWithLineStyle(
    context: CanvasRenderingContext2D, path: Path2D, lineStyle?: LineStyle, lineWidth: number = 1) {
  if (lineStyle && lineStyle.color) {
    context.save();
    context.translate(0.5, 0.5);
    context.lineWidth = lineWidth;
    if (lineStyle.pattern === LinePattern.Dashed) {
      context.setLineDash([3, 3]);
    }
    if (lineStyle.pattern === LinePattern.Dotted) {
      context.setLineDash([2, 2]);
    }
    context.strokeStyle = lineStyle.color;
    context.stroke(path);
    context.restore();
  }
}

export function fillPathWithBoxStyle(
    context: CanvasRenderingContext2D, path: Path2D, bounds: PathBounds, angle: number, boxStyle?: BoxStyle) {
  if (!boxStyle) {
    return;
  }

  context.save();
  if (boxStyle.fillColor) {
    context.fillStyle = boxStyle.fillColor;
    context.fill(path);
  }
  if (boxStyle.hatchColor) {
    hatchFillPath(context, path, bounds, 10, boxStyle.hatchColor, angle, false);
  }
  context.restore();
}

export function buildPath(commands: Array<string|number>, bounds: PathBounds, emulationScaleFactor: number): Path2D {
  let commandsIndex = 0;

  function extractPoints(count: number): number[] {
    const points = [];

    for (let i = 0; i < count; ++i) {
      const x = Math.round(commands[commandsIndex++] as number * emulationScaleFactor);
      bounds.maxX = Math.max(bounds.maxX, x);
      bounds.minX = Math.min(bounds.minX, x);

      const y = Math.round(commands[commandsIndex++] as number * emulationScaleFactor);
      bounds.maxY = Math.max(bounds.maxY, y);
      bounds.minY = Math.min(bounds.minY, y);

      bounds.leftmostXForY[y] = Math.min(bounds.leftmostXForY[y] || Number.MAX_VALUE, x);
      bounds.rightmostXForY[y] = Math.max(bounds.rightmostXForY[y] || Number.MIN_VALUE, x);
      bounds.topmostYForX[x] = Math.min(bounds.topmostYForX[x] || Number.MAX_VALUE, y);
      bounds.bottommostYForX[x] = Math.max(bounds.bottommostYForX[x] || Number.MIN_VALUE, y);

      bounds.allPoints.push({x, y});

      points.push(x, y);
    }

    return points;
  }

  const commandsLength = commands.length;
  const path = new Path2D();
  while (commandsIndex < commandsLength) {
    switch (commands[commandsIndex++]) {
      case 'M':
        path.moveTo.apply(path, extractPoints(1) as [number, number]);
        break;
      case 'L':
        path.lineTo.apply(path, extractPoints(1) as [number, number]);
        break;
      case 'C':
        path.bezierCurveTo.apply(path, extractPoints(3) as [number, number, number, number, number, number]);
        break;
      case 'Q':
        path.quadraticCurveTo.apply(path, extractPoints(2) as [number, number, number, number]);
        break;
      case 'Z':
        path.closePath();
        break;
    }
  }

  return path;
}

export function emptyBounds(): PathBounds {
  const bounds = {
    minX: Number.MAX_VALUE,
    minY: Number.MAX_VALUE,
    maxX: Number.MIN_VALUE,
    maxY: Number.MIN_VALUE,
    leftmostXForY: {},
    rightmostXForY: {},
    topmostYForX: {},
    bottommostYForX: {},
    allPoints: [],
  };
  return bounds;
}

export function applyMatrixToPoint(point: {x: number, y: number}, matrix: DOMMatrix): {x: number, y: number} {
  let domPoint = new DOMPoint(point.x, point.y);
  domPoint = domPoint.matrixTransform(matrix);
  return {x: domPoint.x, y: domPoint.y};
}

const HATCH_LINE_LENGTH = 5;
const HATCH_LINE_GAP = 3;
let hatchLinePattern: CanvasPattern;
let hatchLineColor: string = '';

/**
 * Draw line hatching at a 45 degree angle for a given
 * path.
 *   __________
 *   |\  \  \ |
 *   | \  \  \|
 *   |  \  \  |
 *   |\  \  \ |
 *   **********
 */
export function hatchFillPath(
    context: CanvasRenderingContext2D, path: Path2D, bounds: Bounds, delta: number, color: string,
    rotationAngle: number, flipDirection: boolean|undefined) {
  // Make the bounds be at most the canvas size if it is bigger in any direction.
  // Making the bounds bigger than the canvas is useless as what's drawn there won't be visible.
  if (context.canvas.width < bounds.maxX - bounds.minX || context.canvas.height < bounds.maxY - bounds.minY) {
    bounds = {
      minX: 0,
      maxX: context.canvas.width,
      minY: 0,
      maxY: context.canvas.height,
      allPoints: [],
    };
  }

  // If we haven't done it yet, initialize an offscreen canvas used to create the dashed line repeated pattern.
  if (!hatchLinePattern || color !== hatchLineColor) {
    hatchLineColor = color;

    const offscreenCanvas = document.createElement('canvas');
    offscreenCanvas.width = delta;
    offscreenCanvas.height = HATCH_LINE_LENGTH + HATCH_LINE_GAP;

    const offscreenCtx = offscreenCanvas.getContext('2d') as CanvasRenderingContext2D;
    offscreenCtx.clearRect(0, 0, offscreenCanvas.width, offscreenCanvas.height);
    offscreenCtx.rect(0, 0, 1, HATCH_LINE_LENGTH);
    offscreenCtx.fillStyle = color;
    offscreenCtx.fill();

    hatchLinePattern = context.createPattern(offscreenCanvas, 'repeat') as CanvasPattern;
  }

  context.save();

  const matrix = new DOMMatrix();
  hatchLinePattern.setTransform(matrix.scale(flipDirection ? -1 : 1, 1).rotate(0, 0, -45 + rotationAngle));

  context.fillStyle = hatchLinePattern;
  context.fill(path);

  context.restore();
}

/**
 * Given a quad, create the corresponding path object. This also accepts a list of quads to clip from the resulting
 * path.
 */
export function createPathForQuad(
    outerQuad: Quad, quadsToClip: Quad[], bounds: PathBounds, emulationScaleFactor: number) {
  let commands = [
    'M',
    outerQuad.p1.x,
    outerQuad.p1.y,
    'L',
    outerQuad.p2.x,
    outerQuad.p2.y,
    'L',
    outerQuad.p3.x,
    outerQuad.p3.y,
    'L',
    outerQuad.p4.x,
    outerQuad.p4.y,
  ];
  for (const quad of quadsToClip) {
    commands = [
      ...commands,    'L', quad.p4.x, quad.p4.y, 'L', quad.p3.x, quad.p3.y, 'L', quad.p2.x,
      quad.p2.y,      'L', quad.p1.x, quad.p1.y, 'L', quad.p4.x, quad.p4.y, 'L', outerQuad.p4.x,
      outerQuad.p4.y,
    ];
  }
  commands.push('Z');

  return buildPath(commands, bounds, emulationScaleFactor);
}

export function parseHexa(hexa: string): Array<number> {
  return (hexa.match(/#(\w\w)(\w\w)(\w\w)(\w\w)/) || []).slice(1).map(c => parseInt(c, 16) / 255);
}

export function formatRgba(rgba: number[], colorFormat: 'rgb'|'hsl'): string {
  if (colorFormat === 'rgb') {
    const [r, g, b, a] = rgba;
    // rgb(r g b [ / a])
    return `rgb(${(r * 255).toFixed()} ${(g * 255).toFixed()} ${(b * 255).toFixed()}${
        a === 1 ? '' : ' / ' + Math.round(a * 100) / 100})`;
  }

  if (colorFormat === 'hsl') {
    const [h, s, l, a] = rgbaToHsla(rgba);
    // hsl(hdeg s l [ / a])
    return `hsl(${Math.round(h * 360)}deg ${Math.round(s * 100)} ${Math.round(l * 100)}${
        a === 1 ? '' : ' / ' + Math.round(a * 100) / 100})`;
  }

  throw new Error('NOT_REACHED');
}

export function formatColor(hexa: string, colorFormat: string): string {
  if (colorFormat === 'rgb' || colorFormat === 'hsl') {
    return formatRgba(parseHexa(hexa), colorFormat);
  }

  if (hexa.endsWith('FF')) {
    // short hex if no alpha
    return hexa.substr(0, 7);
  }

  return hexa;
}
