import {promises as fsp} from 'fs';
import {join} from 'path';
import {getIssues, getCommits, getMergeRequests} from './download';

export interface Result {
  count: number;
}

export interface MonthlyResult extends Result {
  month: number;
  year: number;
}

export interface Statistics {
  contributions: {
    authors: number;
    commits: number;
  };
  issues: {
    closed: number;
    opened: number;
  };
  lines: {
    added: number;
    removed: number;
  };
  mergeRequests: {
    closed: number;
    opened: number;
  };
  month: number;
  year: number;
}

async function entry(): Promise<void> {
  const dataDirectory: string = join(__dirname, '../pages/data/');
  await fsp.mkdir(dataDirectory, {recursive: true});

  // Get the GitLab data from a local file or download them from the API.
  const commits: any[] = await getCommits();
  const issues: any[] = await getIssues();
  const mergeRequests: any[] = await getMergeRequests();

  // Get the amount of commits made for each month.
  const commitsMade: MonthlyResult[] = await findData(
    commits,
    () => false,
    (result: MonthlyResult, commit: any) =>
      result.year === new Date(commit.created_at).getFullYear() &&
      result.month === new Date(commit.created_at).getMonth() + 1,
    (commit: any): MonthlyResult => ({
      count: 1,
      year: new Date(commit.created_at).getFullYear(),
      month: new Date(commit.created_at).getMonth() + 1
    }),
    () => 1
  );

  // Get the amount of lines added for each month.
  const linesAdded: MonthlyResult[] = await findData(
    commits,
    () => false,
    (result: MonthlyResult, commit: any) =>
      result.year === new Date(commit.created_at).getFullYear() &&
      result.month === new Date(commit.created_at).getMonth() + 1,
    (commit: any): MonthlyResult => ({
      count: commit.stats.additions,
      year: new Date(commit.created_at).getFullYear(),
      month: new Date(commit.created_at).getMonth() + 1
    }),
    (commit: any): number => commit.stats.additions
  );

  // Get the amount of lines removed for each month.
  const linesRemoved: MonthlyResult[] = await findData(
    commits,
    () => false,
    (result: MonthlyResult, commit: any) =>
      result.year === new Date(commit.created_at).getFullYear() &&
      result.month === new Date(commit.created_at).getMonth() + 1,
    (commit: any): MonthlyResult => ({
      count: commit.stats.deletions,
      year: new Date(commit.created_at).getFullYear(),
      month: new Date(commit.created_at).getMonth() + 1
    }),
    (commit: any): number => commit.stats.deletions
  );

  // Get the amount of issues opened for each month.
  const issuesOpened: MonthlyResult[] = await findData(
    issues,
    () => false,
    (result: MonthlyResult, issue: any) =>
      result.year === new Date(issue.created_at).getFullYear() &&
      result.month === new Date(issue.created_at).getMonth() + 1,
    (issue: any) => ({
      count: 1,
      month: new Date(issue.created_at).getMonth() + 1,
      year: new Date(issue.created_at).getFullYear()
    }),
    () => 1
  );

  // Get the amount of issues closed for each month.
  const issuesClosed: MonthlyResult[] = await findData(
    issues,
    (issue: any) => issue.closed_at === null,
    (result: MonthlyResult, issue: any) =>
      result.year === new Date(issue.closed_at).getFullYear() &&
      result.month === new Date(issue.closed_at).getMonth() + 1,
    (issue: any) => ({
      count: 1,
      month: new Date(issue.closed_at).getMonth() + 1,
      year: new Date(issue.closed_at).getFullYear()
    }),
    () => 1
  );

  // Get the amount of merge requests opened for each month.
  const mergeRequestsOpened: MonthlyResult[] = await findData(
    mergeRequests,
    () => false,
    (result: MonthlyResult, mergeRequest: any) =>
      result.year === new Date(mergeRequest.created_at).getFullYear() &&
      result.month === new Date(mergeRequest.created_at).getMonth() + 1,
    (mergeRequest: any) => ({
      count: 1,
      month: new Date(mergeRequest.created_at).getMonth() + 1,
      year: new Date(mergeRequest.created_at).getFullYear()
    }),
    () => 1
  );

  // Get the amount of merge requests closed/merged for each month.
  const mergeRequestsClosed: MonthlyResult[] = await findData(
    mergeRequests,
    (mergeRequest: any) =>
      mergeRequest.closed_at === null && mergeRequest.merged_at === null,
    (result: MonthlyResult, mergeRequest: any) =>
      result.year ===
        new Date(
          mergeRequest.closed_at === null
            ? mergeRequest.merged_at
            : mergeRequest.closed_at
        ).getFullYear() &&
      result.month ===
        new Date(
          mergeRequest.closed_at === null
            ? mergeRequest.merged_at
            : mergeRequest.closed_at
        ).getMonth() +
          1,
    (mergeRequest: any) => ({
      count: 1,
      month:
        new Date(
          mergeRequest.closed_at === null
            ? mergeRequest.merged_at
            : mergeRequest.closed_at
        ).getMonth() + 1,
      year: new Date(
        mergeRequest.closed_at === null
          ? mergeRequest.merged_at
          : mergeRequest.closed_at
      ).getFullYear()
    }),
    () => 1
  );

  // Create empty statistics for months that may not have any data.
  const statistics: Statistics[] = [];

  // Start at 2018 and end in the current year.
  for (let year = 2018; year < new Date().getFullYear() + 1; year++) {
    for (let month = 1; month < 13; month++) {
      // Tildes started in April of 2018, so don't create anything unless we're in or
      // after that month.
      if (year === 2018 && month < 4) {
        continue;
      }

      // Once we reach the current year and the next month, stop creating more data.
      if (
        year === new Date().getFullYear() &&
        month === new Date().getMonth() + 2
      ) {
        break;
      }

      statistics.push({
        contributions: {
          authors: 0,
          commits: 0
        },
        issues: {
          closed: 0,
          opened: 0
        },
        lines: {
          added: 0,
          removed: 0
        },
        mergeRequests: {
          closed: 0,
          opened: 0
        },
        month,
        year
      });
    }
  }

  for (const statistic of statistics) {
    // Create a `Set()` that will hold all our unique author usernames.
    const uniqueContributors: Set<string> = new Set();
    for (const commit of commits) {
      if (
        statistic.year === new Date(commit.created_at).getFullYear() &&
        statistic.month === new Date(commit.created_at).getMonth() + 1
      ) {
        uniqueContributors.add(commit.author_name);
      }
    }

    // Add all the data to the statistic.
    statistic.contributions.authors = uniqueContributors.size;
    statistic.issues.opened = await getResultCount(
      issuesOpened,
      statistic.year,
      statistic.month
    );
    statistic.contributions.commits = await getResultCount(
      commitsMade,
      statistic.year,
      statistic.month
    );
    statistic.issues.closed = await getResultCount(
      issuesClosed,
      statistic.year,
      statistic.month
    );
    statistic.lines.added = await getResultCount(
      linesAdded,
      statistic.year,
      statistic.month
    );
    statistic.lines.removed = await getResultCount(
      linesRemoved,
      statistic.year,
      statistic.month
    );
    statistic.mergeRequests.closed = await getResultCount(
      mergeRequestsClosed,
      statistic.year,
      statistic.month
    );
    statistic.mergeRequests.opened = await getResultCount(
      mergeRequestsOpened,
      statistic.year,
      statistic.month
    );
  }

  // Sort the statistics ascending by year and month.
  statistics.sort((a, b) => a.year - b.year || a.month - b.month);

  // Write the statistics to file.
  await fsp.writeFile(
    join(dataDirectory, 'statistics.json'),
    JSON.stringify(statistics, null, 2)
  );
}

// A generic function that takes in data and a set of functions to count something from
// that data.
export async function findData<T extends Result>(
  data: any[],
  ignoreFn: (point: any) => boolean,
  findFn: (result: T, point: any) => boolean,
  undefinedFn: (point: any) => T,
  incrementFn: (point: any) => number
): Promise<T[]> {
  const monthlies: T[] = [];
  for (const point of data) {
    // If the ignore function returns true we want to skip the current datapoint.
    if (ignoreFn(point)) {
      continue;
    }

    // Find the monthly that applies to this datapoint.
    const monthly: T | undefined = monthlies.find((result) =>
      findFn(result, point)
    );

    // If it doesn't exist yet we want to create it.
    // Otherwise we want to increment the count.
    if (monthly === undefined) {
      monthlies.push(undefinedFn(point));
    } else {
      monthly.count += incrementFn(point);
    }
  }

  return monthlies;
}

// A function that returns the count of a MonthlyResult in the specified year and month.
async function getResultCount(
  results: MonthlyResult[],
  year: number,
  month: number
): Promise<number> {
  const result: MonthlyResult | undefined = results.find(
    (value) => value.year === year && value.month === month
  );
  return result === undefined ? 0 : result.count;
}

if (require.main === module) {
  void entry();
}
