#!/bin/bash
set -e
set -o pipefail

function error() {
  echo "${1}" 1>&2
  exit 1
}

function setup_project() {
  yarn install
}

function possible_type_dependencies() {
  jq --raw-output \
    '.dependencies + .devDependencies | to_entries | .[] | "@types/" + .key' \
    package.json
}

function has_installable_types() {
  npm view --json "${1}" 2>/dev/null | jq .deprecated | grep --quiet null
}

function installable_type_packages() {
  for possible_type_dependency in $(possible_type_dependencies); do
    if has_installable_types "${possible_type_dependency}"; then
      echo "${possible_type_dependency}"
    fi
  done
}

function react_version() {
  jq --raw-output \
    '.devDependencies.react' \
    package.json
}

function add_types_dependencies() {
  local types_dependencies
  types_dependencies="$(installable_type_packages)"
  if [ -n "${types_dependencies}" ]; then
    echo "${types_dependencies}" | xargs yarn add --dev
  fi

  yarn add -D @types/react@$(react_version)
}

function remove_flow_dependencies() {
  local flow_dependencies
  flow_dependencies="$(jq --raw-output '.dependencies + .devDependencies | keys | .[] | select(contains("flow") or contains("babel"))' package.json)"
  if [ -n "${flow_dependencies}" ]; then
    echo "${flow_dependencies}" | xargs yarn remove
  fi
}

function add_typescript_dependencies() {
  # typescript 4.1.2 is the version used by ts-migrate; if there's
  # mismatch, reignore may not work properly.
  yarn add --dev \
    typescript@4.1.2 \
    @typescript-eslint/parser \
    @typescript-eslint/eslint-plugin \
    ts-jest \
    babel-jest \
    babel-plugin-require-context-hook \
    copyfiles
}

function remove_flow_support() {
  rm -rf flow/ flow-typed/ .flowconfig babel.config.buildjs babel.config.js
}

function remove_empty_files() {
  for directory in jest src utils; do
    find "${directory}" -type f -empty -delete
  done
}

function add_converted_internal_dependencies() {
  echo "No converted internal dependencies to add - skipping."
}

function configure_typescript() {
  if [ -f tsconfig.json ]; then
    rm tsconfig.json
  fi

  # Development configuration
  cat <<EOF > tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "jsx": "react",
    "lib": ["dom", "esnext"],
    "moduleResolution": "node",
    "noEmit": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitReturns": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "skipLibCheck": true,
    "strict": true,
    "target": "es5",
    "esModuleInterop": true,
    "declaration": true,
    "sourceMap": true
  },
  "exclude": [
    "templates/**"
  ]
}
EOF

  # Production configuration: commonjs
  cat <<EOF > tsconfig.build.cjs.json
{
  "extends": "./tsconfig",
  "compilerOptions": {
    "noEmit": false,
    "outDir": "./dist/cjs",
    "rootDir": "src",
    "module": "commonjs",
  },
  "exclude": [
    "utils/**/*",
    "jest/**/*",
    "templates/**/*",
    "**/*.test.ts",
    "**/*.test.tsx",
    "**/*.spec.(ts|tsx)",
    "**/*.stories.(ts|tsx)",
    "**/*.story.(ts|tsx)",
    "src/a11y.shots.ts",
    "src/stories.test.tsx"
  ]
}
EOF

  # Production configuration: ECMAScript Modules
  cat <<EOF > tsconfig.build.esm.json
{
  "extends": "./tsconfig",
  "compilerOptions": {
    "noEmit": false,
    "outDir": "./dist/esm",
    "rootDir": "src",
    "module": "ESNext",
  },
  "exclude": [
    "utils/**/*",
    "jest/**/*",
    "templates/**/*",
    "**/*.test.ts",
    "**/*.test.tsx",
    "**/*.spec.(ts|tsx)",
    "**/*.stories.(ts|tsx)",
    "**/*.story.(ts|tsx)",
    "src/a11y.shots.ts",
    "src/stories.test.tsx"
  ]
}
EOF

  # We'll use the files package field, below.
  rm .npmignore
  jq 'del(.jest)' package.json \
    | jq 'del(.scripts.flow)' \
    | jq '.name = "@wealthsimple/patchwork-ts"' \
    | jq '.scripts.check_types = "tsc --noEmit"' \
    | jq '.scripts."build" = "yarn build:cjs && yarn build:esm"' \
    | jq '.scripts."build:cjs" = "tsc -p tsconfig.build.cjs.json && yarn copyfiles -u 1 \"src/**/*.eot\" \"src/**/*.ttf\" \"src/**/*.woff\" \"src/**/*.woff2\" dist/cjs/"' \
    | jq '.scripts."build:esm" = "tsc -p tsconfig.build.esm.json && yarn copyfiles -u 1 \"src/**/*.eot\" \"src/**/*.ttf\" \"src/**/*.woff\" \"src/**/*.woff2\" dist/esm/"' \
    | jq '.scripts."lint:js" = "eslint ./src --ext .ts --ext .tsx --cache"' \
    | jq '.scripts."lint:css" = "stylelint \"./src/**/*.(ts|tsx)\""' \
    | jq '.scripts.format = "prettier --write \"./{src,utils,.storybook,jest}/**/*.{ts,tsx}\""' \
    | jq '.main = "./dist/cjs/index.js"' \
    | jq '.types = "./dist/cjs/index.d.ts"' \
    | jq '.module = "./dist/esm/index.js"' \
    | jq '.files = ["dist"]' \
    > package.json.tmp

  mv package.json.tmp package.json
}

function configure_jest() {
  cat <<EOF > jest.config.js
module.exports = {
  "snapshotSerializers": [
      "enzyme-to-json/serializer"
    ],
    "testEnvironment": "jsdom",
    "setupFilesAfterEnv": [
      "<rootDir>/jest/setup-tests.ts"
    ],
    "transform": {
      "^.+\\.mdx?$": "@storybook/addon-docs/jest-transform-mdx",
      "^.+\\.(ts|tsx)$": "ts-jest"
    },
    "coverageDirectory": "coverage",
    "collectCoverageFrom": [
      "**/src/**/*.{ts|tsx}",
      "!**/src/**/*.story.{ts|tsx}"
    ],
    "coveragePathIgnorePatterns": [
      "/node_modules/",
      "/jest/",
      "/docs/",
      "/env/",
      "/utils/",
      "/flow/",
      "/templates/"
    ],
    "moduleNameMapper": {
      "\\.css$": "<rootDir>/jest/non-js-module.mock.ts",
      "\\.mk?d": "<rootDir>/jest/non-js-module.mock.ts",
      "\\.eot\\?#iefix$": "<rootDir>/jest/non-js-module.mock.ts",
      "\\.(eot|ttf|woff|woff2)$": "<rootDir>/jest/non-js-module.mock.ts"
    },
    "modulePathIgnorePatterns": [
      "<rootDir>/.yarn-cache/"
    ],
    "preset": "ts-jest",
    "coverageReporters": [
      "lcov"
    ],
    "testMatch": [
      "<rootDir>/src/**/*.test.(ts|tsx)"
    ],
    "globals": {
      "DEFAULT_THEME_NAME": "wealthsimple",
      "STORYBOOK_TITLE": "patchwork",
      "ts-jest": {
        "babelConfig": {
          "plugins": ["babel-plugin-require-context-hook"]
        }
      }
    }
};
EOF
}


function configure_eslint() {
  if [ -f .eslintrc ]; then
    rm .eslintrc
  fi

  yarn add --dev eslint
  
  cat <<EOF > .eslintrc.json
{
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:prettier/recommended"
  ],
  "plugins": ["react", "react-hooks", "jsx-a11y", "import"],
  "parser": "@typescript-eslint/parser",
  "rules": {
    "@typescript-eslint/ban-ts-comment": [
      "error",
      { "ts-ignore": "allow-with-description" }
    ],
    "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
    "@typescript-eslint/no-empty-function": "off",
    "jsx-a11y/anchor-is-valid": [
      "error",
      {
        "components": ["Link"],
        "specialLink": ["onClick"]
      }
    ],
    "arrow-parens": "off",
    "react/self-closing-comp": "off",
    "class-methods-use-this": "off",
    "consistent-return": "off",
    "func-names": "off",
    "func-style": ["warn", "declaration", { "allowArrowFunctions": true }],
    "import/no-extraneous-dependencies": ["error", { "devDependencies": true }],
    "import/no-unresolved": [2, { "ignore": ["#iefix$"] }],
    "import/prefer-default-export": "off",
    "no-alert": "off",
    "no-confusing-arrow": "off",
    "no-plusplus": "off",
    "no-unused-vars": [
      "error",
      { "argsIgnorePattern": "^_", "ignoreRestSiblings": true }
    ],
    "no-prototype-builtins": "off",
    "no-underscore-dangle": "off",
    "strict": "off",
    "global-require": "off",
    "react/jsx-filename-extension": "off",
    "react/require-default-props": "off",
    "react/jsx-closing-bracket-location": "off",
    "react/sort-comp": "off",
    "react/jsx-pascal-case": "off",
    "function-paren-newline": "off",
    "object-curly-newline": "off",
    "react/jsx-one-expression-per-line": "off",
    "react/destructuring-assignment": "off",
    "react/jsx-wrap-multilines": "off",
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn",
    "react/jsx-props-no-spreading": "off",
    "react/jsx-curly-newline": "off"
  },
  "parserOptions": {
    "sourceType": "module"
  },
  "globals": {
    "it": true,
    "expect": true,
    "describe": true,
    "beforeEach": true,
    "before": true,
    "afterEach": true,
    "after": true,
    "jest": true,
    "$Keys": true,
    "$Subtype": true,
    "SyntheticEvent": true,
    "SyntheticInputEvent": true,
    "SyntheticFocusEvent": true,
    "SyntheticKeyboardEvent": true
  },
  "settings": {
    "import/resolver": {
      "node": {
        "extensions": [".js", ".jsx", ".ts", ".tsx"]
      }
    }
  }
}
EOF
}

function format() {
  yarn run format
}

function install_ts_migrate() {
  yarn add --dev ts-migrate
}

function migrate() {
  yarn add --dev flowts

  yarn flowts ./src
  yarn flowts ./utils
  yarn flowts ./jest
  yarn flowts ./templates
}

function expect_errors() {
  yarn ts-migrate reignore .
}

function update_snapshots() {
  yarn test -u
}

function check_types() {
  yarn tsc --noEmit
}

function check_build {
  yarn build --noEmit
}

function cleanup_dependencies() {
  yarn remove ts-migrate flowts
  yarn add --dev yarn-deduplicate
  yarn yarn-deduplicate
}

function lint_fix() {
  yarn lint:js --fix --quiet || true
}

function lint() {
  yarn lint:js --quiet --fix
  yarn lint:css --quiet
}

function fix_spread_types() {
  # FlowTS doesn't handle migrating spread types so well. It always adds a {}
  # which subsequently breaks linting. We can safely find and fix most of these.
  yarn replace-in-files --string=' {} & ' --replacement=' ' "./src/**/*.(ts|tsx)"
}

function fix_tests() {
  yarn replace-in-files --string="import '@babel/polyfill';" --replacement='' ./jest
  yarn replace-in-files --string="/\.stor(ies|y)\.js$/" --replacement="/\.(stories|story)\.(tsx|ts)$/" ./src/stories.test.tsx
  rm ./jest/jest.transform.js
}

function fix_generator() {
  yarn ts-migrate -- rename . -s "./templates/*/**.js"
  
  yarn replace-in-files --string='index.js' --replacement='index.ts' plopfile.js
  yarn replace-in-files --string='component.js' --replacement='component.tsx' plopfile.js
  yarn replace-in-files --string='css-rules.js' --replacement='css-rules.ts' plopfile.js
  yarn replace-in-files --string='proptypes.js' --replacement='proptypes.ts' plopfile.js
  yarn replace-in-files --string='stories.js' --replacement='stories.tsx' plopfile.js
  yarn replace-in-files --string='test.js' --replacement='test.ts' plopfile.js
}

function patch_storybook() {
  yarn replace-in-files --string='@(mdx|js)' --replacement='@(mdx|js|ts|tsx)' .storybook/main.js
  yarn replace-in-files --string='/register' --replacement='/register.tsx' .storybook/main.js
}

function bespoke_fixes() {
  yarn add --dev replace-in-files-cli

  fix_spread_types
  fix_tests
  patch_storybook
  # fix_generator

  yarn remove replace-in-files-cli
}

function supply_custom_types() {
  mkdir @types

  # Lets us import strings from MDX files in storybook.
  cat <<EOF > @types/mdx.d.ts
declare module '*.mdx' {
  const mdxDocs: string;
  export = mdxDocs;
}
EOF

  # We use one function from this package for require.context
  # to autoload stories into Jest
  cat <<EOF > @types/babel-plugin-require-context-hook.d.ts
declare module 'babel-plugin-require-context-hook/register' {
  const registerRequireContextHook: () => void;
  export = registerRequireContextHook;
}
EOF

  # react-super-responsive-table has no accessible typedefs of
  # its own that I can find.
  cat <<EOF > @types/react-super-responsive-table.d.ts
declare module 'react-super-responsive-table' {
  export const Table: any;
  export const Tbody: any;
  export const Thead: any;
  export const Tr: any;
  export const Td: any;
  export const Th: any;
}
EOF

  # Patchwork abuses the module system to ship its own fonts,
  # scoped to the PatchworkThemeProvider resets.
  cat <<EOF > @types/fonts.d.ts
declare module '*.woff' {
  const fontUrl: string;
  export = fontUrl;
}

declare module '*.woff2' {
  const fontUrl: string;
  export = fontUrl;
}

declare module '*.eot' {
  const fontUrl: string;
  export = fontUrl;
}

declare module '*.ttf' {
  const fontUrl: string;
  export = fontUrl;
}
EOF

  # react-intl || @types/react-intl is deprecated.
  # The correct thing to do would be to upgrade react-intl to 5+
  # To unblock distribution, stub everything with any.
cat <<EOF > @types/react-intl.d.ts
declare module 'react-intl' {
  export const FormattedMessage: any;
  export type IntlShape = {
    locale: string;
    formats: any;
    messages: {
      [id: string]: string;
    };
    defaultLocale?: string;
    defaultFormats?: any;
    formatDate: (value: any, options?: any) => string;
    formatTime: (value: any, options?: any) => string;
    formatRelative: (value: any, options?: any) => string;
    formatNumber: (value: any, options?: any) => string;
    formatPlural: (value: any, options?: any) => string;
    formatMessage: (
      messageDescriptor: {
        id: string;
        description?: string;
        defaultMessage?: string;
      },
      values?: any,
    ) => string;
    formatHTMLMessage: (
      messageDescriptor: {
        id: string;
        description?: string;
        defaultMessage?: string;
      },
      values?: any,
    ) => string;
    now: () => number;
  };

  export type InjectIntlProvidedProps = {
    intl: IntlShape;
  };
  export const FormattedNumber: any;
  export const IntlProvider: any;
  export const FormattedHTMLMessage: any;
  export const addLocaleData: any;
};

declare module 'react-intl/locale-data/en'
EOF
}

if git diff --quiet; then
  setup_project
  remove_flow_dependencies
  remove_flow_support
  add_typescript_dependencies
  add_types_dependencies
  add_converted_internal_dependencies
  configure_eslint
  configure_jest
  remove_empty_files
  configure_typescript
  supply_custom_types
  install_ts_migrate
  migrate
  bespoke_fixes
  lint_fix
  expect_errors
  lint
  expect_errors
  check_types
  update_snapshots
  check_build
  cleanup_dependencies
else
  error "There are uncommitted changes. Run this on a clean checkout."
fi
