/**
 * @readonly
 *
 * @enum {string}
 */
export const FrameType = {
  /**
   * ╔════════════╗
   * ║ Hello, TUI ║
   * ╚════════════╝
   *
   * @type {string} Double-lined frame
   */
  Double: "Double",

  /**
   * ┌────────────┐
   * │ Hello, TUI │
   * └────────────┘
   *
   * @type {string} Single-lined frame
   */
  Single: "Single",

  /**
   *
   *   Hello, TUI
   *
   *
   * @type {string} Double-lined frame
   */
  Invisible: "Invisible",

  /**
   * (            )
   * ( Hello, TUI )
   * (            )
   *
   * @type {string} Double-lined frame
   */
  Parentheses: "Parentheses",

  /**
   * +------------+
   * | Hello, TUI |
   * +------------+
   *
   * @type {string} Double-lined frame
   */
  Basic: "Basic",

  /**
   * @protected
   * Default values for the common frame types.
   *
   * [north, south, east, west, northwest, northeast, southwest, southeast]
   */
  values: {
    Basic: "--||++++",
    Double: "══║║╔╗╚╝",
    Invisible: "        ",
    Parentheses: "  ()    ",
    Single: "──││┌┐└┘",
  },
};

export class FramingOptions {
  /** @type {number=0} Vertical Padding; number of vertical whitespace (newlines) between the text and the frame. */
  vPadding = 0;

  /** @type {number=0} Margin ; number of newlines to to insert before and after the framed text */
  vMargin = 0;

  /** @type {number=0} Horizontal Padding; number of whitespace characters to insert between the text and the sides of the frame. */
  hPadding = 0;

  /** @type {number=0} Margin ; number of newlines to to insert before and after the text, but inside the frame */
  hMargin = 0;

  /** @type {FrameType=FrameType.Double} Type of frame to put around the text */
  frameType = FrameType.Double;

  /** @type {number=0} Pad each line to become at least this long */
  minLineWidth = 0;

  // Light block: ░ (U+2591)
  // Medium block: ▒ (U+2592)
  // Dark block: ▓ (U+2593)
  // Solid block: █ (U+2588)
  /** @type {string} Single character to use as filler inside the frame. */
  paddingChar = " "; // character used for padding inside the frame.

  /** @type {string} Single character to use as filler outside the frame. */
  marginChar = " ";

  /** @type {string} The 8 characters that make up the frame elements */
  frameChars = FrameType.values.Double;

  /**
   * @param {object} o
   * @returns {FramingOptions}
   */
  static fromObject(o) {
    const result = new FramingOptions();

    result.vPadding = Math.max(0, Number.parseInt(o.vPadding) || 0);
    result.hPadding = Math.max(0, Number.parseInt(o.hPadding) || 0);
    result.vMargin = Math.max(0, Number.parseInt(o.vMargin) || 0);
    result.hMargin = Math.max(0, Number.parseInt(o.hMargin) || 0);
    result.minLineWidth = Math.max(0, Number.parseInt(o.hMargin) || 0);

    result.paddingChar = String(o.paddingChar || " ")[0] || " ";
    result.marginChar = String(o.marginChar || " ")[0] || " ";

    //
    // Do we have custom and valid frame chars?
    if (
      typeof o.frameChars === "string" &&
      o.frameChars.length === FrameType.values.Double.length
    ) {
      result.frameChars = o.frameChars;

      //
      // do we have document frame type instead ?
    } else if (o.frameType && FrameType.hasOwnProperty(o.frameType)) {
      result.frameChars = FrameType.values[o.frameType];

      // Fall back to using "Double" frame
    } else {
      result.frameChars = FrameType.values.Double;
    }

    return result;
  }
}

/**
 * @param {string|string[]} text the text to be framed. If array, each element will be treated as one line, and they are joined so the whole is to be framed.
 * @param {FramingOptions} options
 */
export function frameText(text, options) {
  if (!options) {
    options = new FramingOptions();
  }

  if (!(options instanceof FramingOptions)) {
    options = FramingOptions.fromObject(options);
  }

  // There is a point to this; each element in the array may contain newlines,
  // so we have to combine everything into a long text and then split into
  // individual lines afterwards.
  if (Array.isArray(text)) {
    text = text.join("\n");
  }

  if (typeof text !== "string") {
    console.debug(text);
    throw new Error(
      `text argument was neither an array or a string, it was a  ${typeof text}`,
    );
  }

  /** @type {string[]} */
  const lines = text.split("\n");

  const innerLineLength = Math.max(
    lines.reduce((accumulator, currentLine) => {
      if (currentLine.length > accumulator) {
        return currentLine.length;
      }
      return accumulator;
    }, 0),
    options.minLineWidth,
  );

  const frameThickness = 1; // always 1 for now.

  const outerLineLength =
    0 +
    innerLineLength +
    frameThickness * 2 +
    options.hPadding * 2 +
    options.hMargin * 2;

  // get the frame characters from the frameType.
  let [
    fNorth, // horizontal frame top lines
    fSouth, // horizontal frame bottom lines
    fWest, // vertical frame lines on the left side
    fEast, // vertical frame lines on the right side
    fNorthWest, // upper left frame corner
    fNorthEast, // upper right frame corner
    fSouthWest, // lower left frame corner
    fSouthEast, // lower right frame corner
  ] = options.frameChars.split("");
  if (fNorth === "§") {
    fNorth = "";
  }
  if (fSouth === "§") {
    fSouth = "";
  }
  if (fEast === "§") {
    fEast = "";
  }
  if (fWest === "§") {
    fWest = "";
  }
  if (fNorthEast === "§") {
    fNorthEast = "";
  }
  if (fSouthEast === "§") {
    fSouthEast = "";
  }
  if (fNorthWest === "§") {
    fNorthWest = "";
  }
  if (fSouthWest === "§") {
    fSouthWest = "";
  }

  let output = "";

  //
  // GENERATE THE MARGIN SPACE ABOVE THE FRAMED MsgType.TEXT
  //
  //  ( we insert space characters even though  )
  //  ( they wouldn't normally be visible. But  )
  //  ( Some fonts might allow us to see blank  )
  //  ( space, and what if we want to nest many )
  //  ( frames inside each other?               )
  //
  output += (options.marginChar.repeat(outerLineLength) + "\n").repeat(
    options.vMargin,
  );

  //
  // GENERATE THE TOP PART OF THE FRAME
  //      ╔════════════╗
  //
  //
  output +=
    "" + // Make sure JS knows we're adding a string.
    options.marginChar.repeat(options.hMargin) + // the margin before the frame starts
    fNorthWest + // northwest frame corner
    fNorth.repeat(innerLineLength + options.hPadding * 2) + // the long horizontal frame top bar
    fNorthEast + // northeast frame corner
    options.marginChar.repeat(options.hMargin) + // the margin after the frame ends
    "\n";
  //
  // GENERATE UPPER PADDING
  //
  //    ║           ║
  //
  //  (the blank lines within the frame and above the text)
  output += (
    options.marginChar.repeat(options.hMargin) +
    fWest +
    options.paddingChar.repeat(innerLineLength + options.hPadding * 2) +
    fEast +
    options.marginChar.repeat(options.hMargin) +
    "\n"
  ).repeat(options.vPadding);

  //
  // GENERATE FRAMED MsgType.TEXT SEGMENT
  //
  //    ║ My pretty ║
  //    ║ text here ║
  //
  // ( this could be done with a reduce() )
  //
  for (const line of lines) {
    output +=
      "" + // Make sure JS knows we're adding a string.
      options.marginChar.repeat(options.hMargin) + // margin before frame
      fWest + // vertical frame char
      options.paddingChar.repeat(options.hPadding) + // padding before text
      line.padEnd(innerLineLength, " ") + // The actual text. Pad it with normal space character, NOT custom space.
      options.paddingChar.repeat(options.hPadding) + // padding after text
      fEast + // vertical frame bar
      options.marginChar.repeat(options.hMargin) + // margin after frame
      "\n";
  }

  //
  // GENERATE LOWER PADDING
  //
  //    ║           ║
  //
  //  ( the blank lines within the )
  //  ( frame and below the text   )
  //
  //  ( this code is a direct      )
  //  ( repeat of the code that    )
  //  ( generates top padding      )
  output += (
    options.marginChar.repeat(options.hMargin) +
    fWest +
    options.paddingChar.repeat(innerLineLength + options.hPadding * 2) +
    fEast +
    options.marginChar.repeat(options.hMargin) +
    "\n"
  ).repeat(options.vPadding);

  //
  // GENERATE THE BOTTOM PART OF THE FRAME
  //
  //      ╚════════════╝
  //
  output +=
    "" + // Make sure JS knows we're adding a string.
    options.marginChar.repeat(options.hMargin) + // the margin before the frame starts
    fSouthWest + // northwest frame corner
    fSouth.repeat(innerLineLength + options.hPadding * 2) + // the long horizontal frame top bar
    fSouthEast + // northeast frame corner
    options.marginChar.repeat(options.hMargin) + // the margin after the frame starts
    "\n";

  //
  // GENERATE THE MARGIN SPACE BELOW THE FRAMED MsgType.TEXT
  //
  //  ( we insert space characters even though  )
  //  ( they wouldn't normally be visible. But  )
  //  ( Some fonts might allow us to see blank  )
  //  ( space, and what if we want to nest many )
  //  ( frames inside each other?               )
  //
  output += (options.marginChar.repeat(outerLineLength) + "\n").repeat(
    options.vMargin,
  );

  return output;
}

// Allow this script to be run directly from node as well as being included!
// https://stackoverflow.com/a/66309132/5622463
