//
// Canvas object & export options
//

"use strict"

const {fileURLToPath} = require('url'),
      {RustClass, core, inspect, argc, REPR} = require('./neon'),
      {Image, ImageData, pixelSize, getSharp} = require('./imagery'),
      {Path2D} = require('./path'),
      {toSkMatrix} = require('./geometry')

class Canvas extends RustClass{
  #contexts

  constructor(width, height, {textContrast=0, textGamma=1.4, gpu=true}={}){
    super(Canvas).alloc({textContrast, textGamma, gpu:!!gpu})
    this.#contexts = []
    Object.assign(this, {width, height})
  }

  getContext(kind){
    return (kind=="2d") ? this.#contexts[0] || this.newPage() : null
  }

  get gpu(){ return this.prop('engine')=='gpu' }
  set gpu(mode){ this.prop('engine', !!mode ? 'gpu' : 'cpu') }

  get engine(){ return JSON.parse(this.prop('engine_status')) }

  get width(){ return this.prop('width') }
  set width(w){
    this.prop('width', !Number.isNaN(+w) && +w>=0 ? w : 300)
    if (this.#contexts[0]) this.getContext("2d").ƒ('resetSize', core(this))
  }

  get height(){ return this.prop('height') }
  set height(h){
    this.prop('height', !Number.isNaN(+h) && +h>=0 ? h : 150)
    if (this.#contexts[0]) this.getContext("2d").ƒ('resetSize', core(this))
  }

  newPage(width, height){
    const {CanvasRenderingContext2D} = require('./context')
    let ctx = new CanvasRenderingContext2D(this)
    this.#contexts.unshift(ctx)
    if (arguments.length==2){
      Object.assign(this, {width, height})
    }
    return ctx
  }

  get pages(){
    return this.#contexts.slice().reverse()
  }

  get raw(){ return this.toBuffer("raw") }
  get png(){ return this.toBuffer("png") }
  get jpg(){ return this.toBuffer("jpg") }
  get pdf(){ return this.toBuffer("pdf") }
  get svg(){ return this.toBuffer("svg") }
  get webp(){ return this.toBuffer("webp") }

  // Warn about renamed methods but map them to the new names (for now)
  saveAs(){ _deprecated('Canvas.saveAs()'); this.toFile(...arguments) }
  saveAsSync(){ _deprecated('Canvas.saveAsSync()'); this.toFileSync(...arguments) }
  toDataURLSync(){ _deprecated('Canvas.toDataURLSync()'); this.toURLSync(...arguments) }

  toFile(filename, opts={}){
    let {pages, padding, pattern, ...rest} = exportOptions(this, {filename}, opts),
        args = [pages.map(core), pattern, padding, rest]
    return this.ƒ("save", ...args)
  }

  toFileSync(filename, opts={}){
    let {pages, padding, pattern, ...rest} = exportOptions(this, {filename}, opts)
    this.ƒ("saveSync", pages.map(core), pattern, padding, rest)
  }

  toBuffer(extension="png", opts={}){
    let {pages, ...rest} = exportOptions(this, {extension}, opts)
    return this.ƒ("toBuffer", pages.map(core), rest)
  }

  toBufferSync(extension="png", opts={}){
    let {pages, ...rest} = exportOptions(this, {extension}, opts)
    return this.ƒ("toBufferSync", pages.map(core), rest)
  }

  toURL(extension="png", opts={}){
    let {mime} = exportOptions(this, {extension}, opts),
        buffer = this.toBuffer(extension, opts);
    return buffer.then(data => `data:${mime};base64,${data.toString('base64')}`)
  }

  toURLSync(extension="png", opts={}){
    let {mime} = exportOptions(this, {extension}, opts),
        buffer = this.toBufferSync(extension, opts);
    return `data:${mime};base64,${buffer.toString('base64')}`
  }

  // Match the browser API in only accepting a single optional quality argument
  toDataURL(extension="png", quality){
    if (quality!==undefined && typeof quality!=='number'){
      throw TypeError("Expected a number in the range 0–1 for `quality` (use toURL() for additional rendering options)")
    }
    return this.toURLSync(extension, {quality})
  }

  toSharp({page, matte, msaa, density=1}={}){
    const {Readable} = require('node:stream'),
          sharp = getSharp(),
          buffer = this.toBuffer("raw", {page, matte, density, msaa})

    return Readable.from(
      (async function * (){ yield buffer })()
    ).pipe(sharp({
      raw: {width:this.width*density, height:this.height*density, channels:4}
    }).withMetadata({density:density * 72}))
  }

  [REPR](depth, options) {
    let {width, height, gpu, engine, pages} = this
    return `Canvas ${inspect({width, height, gpu, engine, pages}, options)}`
  }
}

class CanvasGradient extends RustClass{
  constructor(style, ...coords){
    super(CanvasGradient)
    style = (style || "").toLowerCase()
    if (['linear', 'radial', 'conic'].includes(style)) this.init(style, ...coords)
    else throw new Error(`Function is not a constructor (use CanvasRenderingContext2D's "createConicGradient", "createLinearGradient", and "createRadialGradient" methods instead)`)
  }

  addColorStop(offset, color){
    this.ƒ('addColorStop', ...arguments)
  }

  [REPR](depth, options) {
    return `CanvasGradient (${this.ƒ("repr")})`
  }
}

class CanvasPattern extends RustClass{
  constructor(canvas, src, repeat){
    repeat = [...arguments].slice(2)
    super(CanvasPattern)
    if (src instanceof Image){
      let {width, height} = canvas
      this.init('from_image', core(src), width, height, ...repeat)
    }else if (src instanceof ImageData){
      this.init('from_image_data', src, ...repeat)
    }else if (src instanceof Canvas){
      let ctx = src.getContext('2d')
      this.init('from_canvas', core(ctx), ...repeat)
    }else{
      throw new Error("CanvasPatterns require a source Image or a Canvas")
    }
  }

  setTransform(matrix) { this.ƒ('setTransform', toSkMatrix.apply(null, arguments)) }

  [REPR](depth, options) {
    return `CanvasPattern (${this.ƒ("repr")})`
  }
}

class CanvasTexture extends RustClass{
  constructor(spacing, {path, color, angle, line, cap="butt", outline=false, offset=0}={}){
    super(CanvasTexture)
    argc(arguments, 1)
    let [x, y] = Array.isArray(offset) ? offset.concat(offset).slice(0, 2) : [offset, offset]
    let [h, v] = Array.isArray(spacing) ? spacing.concat(spacing).slice(0, 2) : [spacing, spacing]
    if (path!==undefined && !(path instanceof Path2D)){
      throw TypeError("Expected a Path2D object for `path`")
    }
    path = core(path)
    line = line != null ? line : (path ? 0 : 1)
    angle = angle != null ? angle : (path ? 0 : -Math.PI / 4)
    this.alloc(path, color, line, cap, angle, !!outline, h, v, x, y)
  }

  [REPR](depth, options) {
    return `CanvasTexture (${this.ƒ("repr")})`
  }
}


//
// Mime type <-> File extension mappings
//

class Format{
  constructor(){
    let png = "image/png",
        jpg = "image/jpeg",
        jpeg = "image/jpeg",
        webp = "image/webp",
        pdf = "application/pdf",
        svg = "image/svg+xml",
        raw = "application/octet-stream"

    Object.assign(this, {
      toMime: this.toMime.bind(this),
      fromMime: this.fromMime.bind(this),
      expected: `"png", "jpg", "webp", "raw", "pdf", or "svg"`,
      formats: {png, jpg, jpeg, webp, raw, pdf, svg},
      mimes: {[png]: "png", [jpg]: "jpg", [webp]: "webp", [raw]: "raw", [pdf]: "pdf", [svg]: "svg"},
    })
  }

  toMime(ext){
    return this.formats[(ext||'').replace(/^\./, '').toLowerCase()]
  }

  fromMime(mime){
    return this.mimes[mime]
  }
}

//
// Validation of the options dict shared by the `saveAs`, `toBuffer`, and `toDataURL` methods
//

const {basename, extname} = require('path')

function exportOptions(canvas, {filename='', extension=''}, opts){
  // a single number will be interpreted as a quality setting
  if (typeof opts=='number') opts = {quality:opts}

  // unpack common export options
  let {page, quality, matte, density, msaa, outline, downsample, colorType} = opts

  // only allow format overrides in toFile()
  let imageFormat = !!filename ? opts.format : undefined

  if (filename instanceof URL){
    if (filename.protocol=='file:') filename = fileURLToPath(filename)
    else throw Error(`URLs must use 'file' protocol (got '${filename.protocol.replace(':', '')}')`)
  }

  // ensure the canvas has a context (so it can at least generate an empty image)
  if (!canvas.pages.length) canvas.getContext("2d")

  var {fromMime, toMime, expected} = new Format(),
      ext = imageFormat || extension.replace(/@\d+x$/i,'') || extname(filename),
      format = fromMime(toMime(ext) || ext),
      mime = toMime(format),
      pages = canvas.pages,
      pp = pages.length

  if (!ext) throw new Error(`Cannot determine image format (use a filename extension or 'format' argument)`)
  if (!format) throw new Error(`Unsupported file format "${ext}" (expected ${expected})`)

  let padding, isSequence, pattern = filename.replace(/{(\d*)}/g, (_, width) => {
    isSequence = true
    width = parseInt(width, 10)
    padding = isFinite(width) ? width : isFinite(padding) ? padding : -1
    return "{}"
  })

  // allow negative indexing if a specific page is specified
  let idx = page > 0 ? page - 1
          : page < 0 ? pp + page
          : undefined;

  if (isFinite(idx) && idx < 0 || idx >= pp) throw new RangeError(
    pp == 1 ? `Canvas only has a ‘page 1’ (${idx} is out of bounds)`
            : `Canvas has pages 1–${pp} (${idx} is out of bounds)`
  )

  pages = isFinite(idx) ? [pages[idx]]
        : isSequence || format=='pdf' ? pages
        : pages.slice(-1) // default to the 'current' context

  // inherit text settings from the canvas (since they can't be changed on a per-render basis due to glyph caching)
  const {textContrast, textGamma} = canvas.engine

  if (quality===undefined){
    quality = 0.92
  }else{
    if (typeof quality!='number' || !isFinite(quality) || quality<0 || quality>1){
      throw new TypeError("Expected a number between 0.0–1.0 for `quality`")
    }
  }

  if (density===undefined){
    let m = (extension || basename(filename, ext)).match(/@(\d+)x$/i)
    density = m ? parseInt(m[1], 10) : 1
  }else if (typeof density!='number' || !Number.isInteger(density) || density<1){
    throw new TypeError("Expected a non-negative integer for `density`")
  }

  if (msaa===undefined || msaa===true) {
    msaa = undefined // use the default 4x msaa
  }else if (!isFinite(+msaa) || +msaa<0){
    throw new TypeError("The number of MSAA samples must be an integer ≥0")
  }

  if (colorType!==undefined){
    pixelSize(colorType) // throw an error if invalid
  }

  // default to false, otherwise detect truthy
  downsample = !!downsample
  outline = !!outline

  return {
    filename, pattern, format, mime, pages, padding, quality, matte,
    density, msaa, outline, textContrast, textGamma, downsample, colorType
  }
}

// emit a deprecation warning, once per API per process
let _warnings = {
  "Canvas.saveAs()": "Canvas.toFile()",
  "Canvas.saveAsSync()": "Canvas.toFileSync()",
  "Canvas.toDataURLSync()": "Canvas.toURLSync() (see also Canvas.toDataURL() which is now synchronous)",
}
function _deprecated(oldAPI){
  let newAPI = _warnings[oldAPI]
  if (newAPI) console.error(`Deprecation warning: ${oldAPI} has been renamed to ${newAPI} and will stop working in a future release.`)
  delete _warnings[oldAPI]
}

module.exports = {Canvas, CanvasGradient, CanvasPattern, CanvasTexture, getSharp}
