import path from "path";
import {
  AbsoluteImportDatum,
  RelativeImportDatum,
  ImportDatum,
  StaticResourceData,
  RenderKeyTuple,
  RenderType,
} from "./types";
import { posixPath } from "../utils";
import { resourceToHex } from "../../resourceToHex";
import { hexToResource } from "../../hexToResource";

/**
 * Common header for all codegenerated solidity files
 */
export const renderedSolidityHeader = `// SPDX-License-Identifier: MIT
pragma solidity >=0.8.24;

/* Autogenerated file. Do not edit manually. */`;

/**
 * Renders a list of lines
 */
export function renderList<T>(list: T[], renderItem: (item: T, index: number) => string): string {
  return internalRenderList("", list, renderItem);
}

/**
 * Renders a comma-separated list of arguments for solidity functions, ignoring empty and undefined ones
 */
export function renderArguments(args: (string | undefined)[]): string {
  const filteredArgs = args.filter((arg) => arg !== undefined && arg !== "") as string[];
  return internalRenderList(",", filteredArgs, (arg) => arg);
}

interface RenderedCommonData {
  /** `_tableId` variable prefixed with its type (empty string if absent) */
  _typedTableId: string;
  /** Comma-separated table key names prefixed with their types (empty string if 0 keys) */
  _typedKeyArgs: string;
  /** Definition and initialization of the dynamic `_keyTuple` bytes32 array */
  _keyTupleDefinition: string;
}

/**
 * Renders some solidity statements commonly used within table libraries
 * @param param0.staticResourceData static data about the table library
 * @param param0.keyTuple key tuple of the table library
 * @returns Rendered statement strings
 */
export function renderCommonData({
  staticResourceData,
  keyTuple,
}: {
  staticResourceData?: StaticResourceData;
  keyTuple: RenderKeyTuple[];
}): RenderedCommonData {
  // static resource means static tableId as well, and no tableId arguments
  const _typedTableId = staticResourceData ? "" : "ResourceId _tableId";
  const _typedKeyArgs = renderArguments(keyTuple.map(({ name, typeWithLocation }) => `${typeWithLocation} ${name}`));

  const _keyTupleDefinition = `
    bytes32[] memory _keyTuple = new bytes32[](${keyTuple.length});
    ${renderList(keyTuple, (key, index) => `_keyTuple[${index}] = ${renderValueTypeToBytes32(key.name, key)};`)}
  `;

  return {
    _typedTableId,
    _typedKeyArgs,
    _keyTupleDefinition,
  };
}

/** For 2 paths which are relative to a common root, create a relative import path from one to another */
export function solidityRelativeImportPath(fromPath: string, usedInPath: string): string {
  // 1st "./" must be added because path strips it,
  // but solidity expects it unless there's "../" ("./../" is fine).
  // 2nd and 3rd "./" forcefully avoid absolute paths (everything is relative to `src`).
  return posixPath("./" + path.relative("./" + usedInPath, "./" + fromPath));
}

/**
 * Aggregates, deduplicates and renders imports for symbols per path.
 * Identical symbols from different paths are NOT handled, they should be checked before rendering.
 */
export function renderImports(imports: ImportDatum[]): string {
  return renderAbsoluteImports(
    imports.map((importDatum) => {
      if ("path" in importDatum) {
        return importDatum;
      } else {
        return {
          symbol: importDatum.symbol,
          path: solidityRelativeImportPath(importDatum.fromPath, importDatum.usedInPath),
        };
      }
    }),
  );
}

/**
 * Aggregates, deduplicates and renders imports for symbols per path.
 * Identical symbols from different paths are NOT handled, they should be checked before rendering.
 */
export function renderRelativeImports(imports: RelativeImportDatum[]): string {
  return renderAbsoluteImports(
    imports.map(({ symbol, fromPath, usedInPath }) => ({
      symbol,
      path: solidityRelativeImportPath(fromPath, usedInPath),
    })),
  );
}

/**
 * Aggregates, deduplicates and renders imports for symbols per path.
 * Identical symbols from different paths are NOT handled, they should be checked before rendering.
 */
export function renderAbsoluteImports(imports: AbsoluteImportDatum[]): string {
  // Aggregate symbols by import path, also deduplicating them
  const aggregatedImports = new Map<string, Set<string>>();
  for (const { symbol, path } of imports) {
    if (!aggregatedImports.has(path)) {
      aggregatedImports.set(path, new Set());
    }
    aggregatedImports.get(path)?.add(symbol);
  }
  // Render imports
  const renderedImports = [];
  for (const [path, symbols] of aggregatedImports) {
    const renderedSymbols = [...symbols].join(", ");
    renderedImports.push(`import { ${renderedSymbols} } from "${posixPath(path)}";`);
  }
  return renderedImports.join("\n");
}

interface RenderWithStoreCallbackData {
  /** `_store` variable prefixed with its type (undefined if library name) */
  _typedStore: string | undefined;
  /**  `_store` variable (undefined if library name) */
  _store: string;
  /** Empty string if storeArgument is false, otherwise `" (using the specified store)"` */
  _commentSuffix: string;
  /** Prefix to differentiate different kinds of store usage within methods */
  _methodNamePrefix: string;
  /** Whether FieldLayout variable should be passed to store methods */
  _useExplicitFieldLayout?: boolean;
}

/**
 * Renders several versions of the callback's result, which access Store in different ways
 * @param storeArgument whether to render a version with `IStore _store` as an argument
 * @param callback renderer for a method which uses store
 * @returns Concatenated results of all callback calls
 */
export function renderWithStore(
  storeArgument: boolean,
  callback: (data: RenderWithStoreCallbackData) => string,
): string {
  let result = "";
  result += callback({ _typedStore: undefined, _store: "StoreSwitch", _commentSuffix: "", _methodNamePrefix: "" });
  result += callback({
    _typedStore: undefined,
    _store: "StoreCore",
    _commentSuffix: "",
    _methodNamePrefix: "_",
    _useExplicitFieldLayout: true,
  });

  if (storeArgument) {
    result +=
      "\n" +
      callback({
        _typedStore: "IStore _store",
        _store: "_store",
        _commentSuffix: " (using the specified store)",
        _methodNamePrefix: "",
      });
  }

  return result;
}

/**
 * Renders several versions of the callback's result, which have different method name suffixes
 * @param withSuffixlessFieldMethods whether to render methods with an empty suffix
 * @param fieldName name of the field which the methods access, used for a suffix
 * @param callback renderer for a method to be suffixed
 * @returns Concatenated results of all callback calls
 */
export function renderWithFieldSuffix(
  withSuffixlessFieldMethods: boolean,
  fieldName: string,
  callback: (_methodNameSuffix: string) => string,
): string {
  const methodNameSuffix = `${fieldName[0].toUpperCase()}${fieldName.slice(1)}`;
  let result = "";
  result += callback(methodNameSuffix);

  if (withSuffixlessFieldMethods) {
    result += "\n" + callback("");
  }

  return result;
}

/**
 * Renders `_tableId` definition of the given table.
 * @param param0 static resource data needed to construct the table ID
 */
export function renderTableId({
  namespace,
  name,
  offchainOnly,
}: Pick<StaticResourceData, "namespace" | "name" | "offchainOnly">): string {
  const tableId = resourceToHex({
    type: offchainOnly ? "offchainTable" : "table",
    namespace,
    name,
  });
  // turn table ID back into arguments that would be valid in `WorldResourceIdLib.encode` (like truncated names)
  const resource = hexToResource(tableId);
  return `
    // Hex below is the result of \`WorldResourceIdLib.encode({ namespace: ${JSON.stringify(
      resource.namespace,
    )}, name: ${JSON.stringify(resource.name)}, typeId: ${offchainOnly ? "RESOURCE_OFFCHAIN_TABLE" : "RESOURCE_TABLE"} });\`
    ResourceId constant _tableId = ResourceId.wrap(${tableId});
  `;
}

/**
 * Renders solidity typecasts to get from the given type to `bytes32`
 * @param name variable name to be typecasted
 * @param param1 type data
 */
export function renderValueTypeToBytes32(
  name: string,
  { typeUnwrap, internalTypeId }: Pick<RenderType, "typeUnwrap" | "internalTypeId">,
): string {
  const innerText = typeUnwrap.length ? `${typeUnwrap}(${name})` : name;

  if (internalTypeId === "bytes32") {
    return innerText;
  } else if (/^bytes\d{1,2}$/.test(internalTypeId)) {
    return `bytes32(${innerText})`;
  } else if (/^uint\d{1,3}$/.test(internalTypeId)) {
    return `bytes32(uint256(${innerText}))`;
  } else if (/^int\d{1,3}$/.test(internalTypeId)) {
    return `bytes32(uint256(int256(${innerText})))`;
  } else if (internalTypeId === "address") {
    return `bytes32(uint256(uint160(${innerText})))`;
  } else if (internalTypeId === "bool") {
    return `_boolToBytes32(${innerText})`;
  } else {
    throw new Error(`Unknown value type id ${internalTypeId}`);
  }
}

/**
 * Whether the storage representation of the given solidity type is left aligned
 */
export function isLeftAligned(field: Pick<RenderType, "internalTypeId">): boolean {
  return /^bytes\d{1,2}$/.test(field.internalTypeId);
}

/**
 * The number of padding bits in the storage representation of a right-aligned solidity type
 */
export function getLeftPaddingBits(field: Pick<RenderType, "internalTypeId" | "staticByteLength">): number {
  if (isLeftAligned(field)) {
    return 0;
  } else {
    return 256 - field.staticByteLength * 8;
  }
}

/**
 * Internal helper to render `lineTerminator`-separated list of items mapped by `renderItem`
 */
function internalRenderList<T>(
  lineTerminator: string,
  list: T[],
  renderItem: (item: T, index: number) => string,
): string {
  return list
    .map((item, index) => renderItem(item, index) + (index === list.length - 1 ? "" : lineTerminator))
    .join("\n");
}
