<script lang="ts">
import {nextTick, defineComponent} from 'vue';
import {SvgIcon} from '../svg.ts';
import {GET} from '../modules/fetch.ts';
import {fomanticQuery} from '../modules/fomantic/base.ts';

const {appSubUrl, assetUrlPrefix, pageData} = window.config;

type CommitStatus = 'pending' | 'success' | 'error' | 'failure' | 'warning' | 'skipped';

type CommitStatusMap = {
  [status in CommitStatus]: {
    name: string,
    color: string,
  };
};

// make sure this matches templates/repo/commit_status.tmpl
const commitStatus: CommitStatusMap = {
  pending: {name: 'octicon-dot-fill', color: 'yellow'},
  success: {name: 'octicon-check', color: 'green'},
  error: {name: 'gitea-exclamation', color: 'red'},
  failure: {name: 'octicon-x', color: 'red'},
  warning: {name: 'gitea-exclamation', color: 'yellow'},
  skipped: {name: 'octicon-skip', color: 'grey'},
};

export default defineComponent({
  components: {SvgIcon},
  data() {
    const params = new URLSearchParams(window.location.search);
    const tab = params.get('repo-search-tab') || 'repos';
    const reposFilter = params.get('repo-search-filter') || 'all';
    const privateFilter = params.get('repo-search-private') || 'both';
    const archivedFilter = params.get('repo-search-archived') || 'unarchived';
    const searchQuery = params.get('repo-search-query') || '';
    const page = Number(params.get('repo-search-page')) || 1;

    return {
      tab,
      repos: [],
      reposTotalCount: null,
      reposFilter,
      archivedFilter,
      privateFilter,
      page,
      finalPage: 1,
      searchQuery,
      isLoading: false,
      staticPrefix: assetUrlPrefix,
      counts: {},
      repoTypes: {
        all: {
          searchMode: '',
        },
        forks: {
          searchMode: 'fork',
        },
        mirrors: {
          searchMode: 'mirror',
        },
        sources: {
          searchMode: 'source',
        },
        collaborative: {
          searchMode: 'collaborative',
        },
      },
      textArchivedFilterTitles: {},
      textPrivateFilterTitles: {},

      organizations: [],
      isOrganization: true,
      canCreateOrganization: false,
      organizationsTotalCount: 0,
      organizationId: 0,

      subUrl: appSubUrl,
      ...pageData.dashboardRepoList,
      activeIndex: -1, // don't select anything at load, first cursor down will select
    };
  },

  computed: {
    showMoreReposLink() {
      return this.repos.length > 0 && this.repos.length < this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`];
    },
    searchURL() {
      return `${this.subUrl}/repo/search?sort=updated&order=desc&uid=${this.uid}&team_id=${this.teamId}&q=${this.searchQuery
      }&page=${this.page}&limit=${this.searchLimit}&mode=${this.repoTypes[this.reposFilter].searchMode
      }${this.archivedFilter === 'archived' ? '&archived=true' : ''}${this.archivedFilter === 'unarchived' ? '&archived=false' : ''
      }${this.privateFilter === 'private' ? '&is_private=true' : ''}${this.privateFilter === 'public' ? '&is_private=false' : ''
      }`;
    },
    repoTypeCount() {
      return this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`];
    },
    checkboxArchivedFilterTitle() {
      return this.textArchivedFilterTitles[this.archivedFilter];
    },
    checkboxArchivedFilterProps() {
      return {checked: this.archivedFilter === 'archived', indeterminate: this.archivedFilter === 'both'};
    },
    checkboxPrivateFilterTitle() {
      return this.textPrivateFilterTitles[this.privateFilter];
    },
    checkboxPrivateFilterProps() {
      return {checked: this.privateFilter === 'private', indeterminate: this.privateFilter === 'both'};
    },
  },

  mounted() {
    const el = document.querySelector('#dashboard-repo-list');
    this.changeReposFilter(this.reposFilter);
    fomanticQuery(el.querySelector('.ui.dropdown')).dropdown();

    this.textArchivedFilterTitles = {
      'archived': this.textShowOnlyArchived,
      'unarchived': this.textShowOnlyUnarchived,
      'both': this.textShowBothArchivedUnarchived,
    };

    this.textPrivateFilterTitles = {
      'private': this.textShowOnlyPrivate,
      'public': this.textShowOnlyPublic,
      'both': this.textShowBothPrivatePublic,
    };
  },

  methods: {
    changeTab(tab: string) {
      this.tab = tab;
      this.updateHistory();
    },

    changeReposFilter(filter: string) {
      this.reposFilter = filter;
      this.repos = [];
      this.page = 1;
      this.counts[`${filter}:${this.archivedFilter}:${this.privateFilter}`] = 0;
      this.searchRepos();
    },

    updateHistory() {
      const params = new URLSearchParams(window.location.search);

      if (this.tab === 'repos') {
        params.delete('repo-search-tab');
      } else {
        params.set('repo-search-tab', this.tab);
      }

      if (this.reposFilter === 'all') {
        params.delete('repo-search-filter');
      } else {
        params.set('repo-search-filter', this.reposFilter);
      }

      if (this.privateFilter === 'both') {
        params.delete('repo-search-private');
      } else {
        params.set('repo-search-private', this.privateFilter);
      }

      if (this.archivedFilter === 'unarchived') {
        params.delete('repo-search-archived');
      } else {
        params.set('repo-search-archived', this.archivedFilter);
      }

      if (this.searchQuery === '') {
        params.delete('repo-search-query');
      } else {
        params.set('repo-search-query', this.searchQuery);
      }

      if (this.page === 1) {
        params.delete('repo-search-page');
      } else {
        params.set('repo-search-page', `${this.page}`);
      }

      const queryString = params.toString();
      if (queryString) {
        window.history.replaceState({}, '', `?${queryString}`);
      } else {
        window.history.replaceState({}, '', window.location.pathname);
      }
    },

    toggleArchivedFilter() {
      if (this.archivedFilter === 'unarchived') {
        this.archivedFilter = 'archived';
      } else if (this.archivedFilter === 'archived') {
        this.archivedFilter = 'both';
      } else { // including both
        this.archivedFilter = 'unarchived';
      }
      this.page = 1;
      this.repos = [];
      this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`] = 0;
      this.searchRepos();
    },

    togglePrivateFilter() {
      if (this.privateFilter === 'both') {
        this.privateFilter = 'public';
      } else if (this.privateFilter === 'public') {
        this.privateFilter = 'private';
      } else { // including private
        this.privateFilter = 'both';
      }
      this.page = 1;
      this.repos = [];
      this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`] = 0;
      this.searchRepos();
    },

    async changePage(page: number) {
      if (this.isLoading) return;

      this.page = page;
      if (this.page > this.finalPage) {
        this.page = this.finalPage;
      }
      if (this.page < 1) {
        this.page = 1;
      }
      this.repos = [];
      this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`] = 0;
      await this.searchRepos();
    },

    async searchRepos() {
      this.isLoading = true;

      const searchedMode = this.repoTypes[this.reposFilter].searchMode;
      const searchedURL = this.searchURL;
      const searchedQuery = this.searchQuery;

      let response, json;
      try {
        const firstLoad = this.reposTotalCount === null;
        if (!this.reposTotalCount) {
          const totalCountSearchURL = `${this.subUrl}/repo/search?count_only=1&uid=${this.uid}&team_id=${this.teamId}&q=&page=1&mode=`;
          response = await GET(totalCountSearchURL);
          this.reposTotalCount = parseInt(response.headers.get('X-Total-Count') ?? '0');
        }
        if (firstLoad && this.reposTotalCount) {
          nextTick(() => {
            // MDN: If there's no focused element, this is the Document.body or Document.documentElement.
            if ((document.activeElement === document.body || document.activeElement === document.documentElement)) {
              this.$refs.search.focus({preventScroll: true});
            }
          });
        }
        response = await GET(searchedURL);
        json = await response.json();
      } catch {
        if (searchedURL === this.searchURL) {
          this.isLoading = false;
        }
        return;
      }

      if (searchedURL === this.searchURL) {
        this.repos = json.data.map((webSearchRepo: any) => {
          return {
            ...webSearchRepo.repository,
            latest_commit_status_state: webSearchRepo.latest_commit_status?.State, // if latest_commit_status is null, it means there is no commit status
            latest_commit_status_state_link: webSearchRepo.latest_commit_status?.TargetURL,
            locale_latest_commit_status_state: webSearchRepo.locale_latest_commit_status,
          };
        });
        const count = Number(response.headers.get('X-Total-Count'));
        if (searchedQuery === '' && searchedMode === '' && this.archivedFilter === 'both') {
          this.reposTotalCount = count;
        }
        this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`] = count;
        this.finalPage = Math.ceil(count / this.searchLimit);
        this.updateHistory();
        this.isLoading = false;
      }
    },

    repoIcon(repo: any) {
      if (repo.fork) {
        return 'octicon-repo-forked';
      } else if (repo.mirror) {
        return 'octicon-mirror';
      } else if (repo.template) {
        return `octicon-repo-template`;
      } else if (repo.private) {
        return 'octicon-lock';
      } else if (repo.internal) {
        return 'octicon-repo';
      }
      return 'octicon-repo';
    },

    statusIcon(status: CommitStatus) {
      return commitStatus[status].name;
    },

    statusColor(status: CommitStatus) {
      return commitStatus[status].color;
    },

    async reposFilterKeyControl(e: KeyboardEvent) {
      switch (e.key) {
        case 'Enter':
          document.querySelector<HTMLAnchorElement>('.repo-owner-name-list li.active a')?.click();
          break;
        case 'ArrowUp':
          if (this.activeIndex > 0) {
            this.activeIndex--;
          } else if (this.page > 1) {
            await this.changePage(this.page - 1);
            this.activeIndex = this.searchLimit - 1;
          }
          break;
        case 'ArrowDown':
          if (this.activeIndex < this.repos.length - 1) {
            this.activeIndex++;
          } else if (this.page < this.finalPage) {
            this.activeIndex = 0;
            await this.changePage(this.page + 1);
          }
          break;
        case 'ArrowRight':
          if (this.page < this.finalPage) {
            await this.changePage(this.page + 1);
          }
          break;
        case 'ArrowLeft':
          if (this.page > 1) {
            await this.changePage(this.page - 1);
          }
          break;
      }
      if (this.activeIndex === -1 || this.activeIndex > this.repos.length - 1) {
        this.activeIndex = 0;
      }
    },
  },
});
</script>
<template>
  <div>
    <div v-if="!isOrganization" class="ui two item menu">
      <a :class="{item: true, active: tab === 'repos'}" @click="changeTab('repos')">{{ textRepository }}</a>
      <a :class="{item: true, active: tab === 'organizations'}" @click="changeTab('organizations')">{{ textOrganization }}</a>
    </div>
    <div v-show="tab === 'repos'" class="ui tab active list dashboard-repos">
      <h4 class="ui top attached header tw-flex tw-items-center">
        <div class="tw-flex-1 tw-flex tw-items-center">
          {{ textMyRepos }}
          <span v-if="reposTotalCount" class="ui grey label tw-ml-2">{{ reposTotalCount }}</span>
        </div>
        <a class="tw-flex tw-items-center muted" :href="subUrl + '/repo/create' + (isOrganization ? '?org=' + organizationId : '')" :data-tooltip-content="textNewRepo">
          <svg-icon name="octicon-plus"/>
        </a>
      </h4>
      <div v-if="!reposTotalCount" class="ui attached segment">
        <div v-if="!isLoading" class="empty-repo-or-org">
          <svg-icon name="octicon-git-branch" :size="24"/>
          <p>{{ textNoRepo }}</p>
        </div>
        <!-- using the loading indicator here will cause more (unnecessary) page flickers, so at the moment, not use the loading indicator -->
        <!-- <div v-else class="is-loading loading-icon-2px tw-min-h-16"/> -->
      </div>
      <div v-else class="ui attached segment repos-search">
        <div class="ui small fluid action left icon input">
          <input type="search" spellcheck="false" maxlength="255" @input="changeReposFilter(reposFilter)" v-model="searchQuery" ref="search" @keydown="reposFilterKeyControl" :placeholder="textSearchRepos">
          <i class="icon loading-icon-3px" :class="{'is-loading': isLoading}"><svg-icon name="octicon-search" :size="16"/></i>
          <div class="ui dropdown icon button" :title="textFilter">
            <svg-icon name="octicon-filter" :size="16"/>
            <div class="menu">
              <a class="item" @click="toggleArchivedFilter()">
                <div class="ui checkbox" ref="checkboxArchivedFilter" :title="checkboxArchivedFilterTitle">
                  <!--the "tw-pointer-events-none" is necessary to prevent the checkbox from handling user's input,
                      otherwise if the "input" handles click event for intermediate status, it breaks the internal state-->
                  <input type="checkbox" class="tw-pointer-events-none" v-bind.prop="checkboxArchivedFilterProps">
                  <label>
                    <svg-icon name="octicon-archive" :size="16" class="tw-mr-1"/>
                    {{ textShowArchived }}
                  </label>
                </div>
              </a>
              <a class="item" @click="togglePrivateFilter()">
                <div class="ui checkbox" ref="checkboxPrivateFilter" :title="checkboxPrivateFilterTitle">
                  <input type="checkbox" class="tw-pointer-events-none" v-bind.prop="checkboxPrivateFilterProps">
                  <label>
                    <svg-icon name="octicon-lock" :size="16" class="tw-mr-1"/>
                    {{ textShowPrivate }}
                  </label>
                </div>
              </a>
            </div>
          </div>
        </div>
        <overflow-menu class="ui secondary pointing tabular borderless menu repos-filter">
          <div class="overflow-menu-items tw-justify-center">
            <a class="item" tabindex="0" :class="{active: reposFilter === 'all'}" @click="changeReposFilter('all')">
              {{ textAll }}
              <div v-show="reposFilter === 'all'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
            </a>
            <a class="item" tabindex="0" :class="{active: reposFilter === 'sources'}" @click="changeReposFilter('sources')">
              {{ textSources }}
              <div v-show="reposFilter === 'sources'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
            </a>
            <a class="item" tabindex="0" :class="{active: reposFilter === 'forks'}" @click="changeReposFilter('forks')">
              {{ textForks }}
              <div v-show="reposFilter === 'forks'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
            </a>
            <a class="item" tabindex="0" :class="{active: reposFilter === 'mirrors'}" @click="changeReposFilter('mirrors')" v-if="isMirrorsEnabled">
              {{ textMirrors }}
              <div v-show="reposFilter === 'mirrors'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
            </a>
            <a class="item" tabindex="0" :class="{active: reposFilter === 'collaborative'}" @click="changeReposFilter('collaborative')">
              {{ textCollaborative }}
              <div v-show="reposFilter === 'collaborative'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
            </a>
          </div>
        </overflow-menu>
      </div>
      <div v-if="repos.length" class="ui attached table segment tw-rounded-b">
        <ul class="repo-owner-name-list">
          <li class="tw-flex tw-items-center tw-py-2" v-for="(repo, index) in repos" :class="{'active': index === activeIndex}" :key="repo.id">
            <a class="repo-list-link muted" :href="repo.link">
              <svg-icon :name="repoIcon(repo)" :size="16" class="repo-list-icon"/>
              <div class="text truncate">{{ repo.full_name }}</div>
              <div v-if="repo.archived">
                <svg-icon name="octicon-archive" :size="16"/>
              </div>
            </a>
            <a class="tw-flex tw-items-center" v-if="repo.latest_commit_status_state" :href="repo.latest_commit_status_state_link || null" :data-tooltip-content="repo.locale_latest_commit_status_state">
              <!-- the commit status icon logic is taken from templates/repo/commit_status.tmpl -->
              <svg-icon :name="statusIcon(repo.latest_commit_status_state)" :class="'tw-ml-2 commit-status icon text ' + statusColor(repo.latest_commit_status_state)" :size="16"/>
            </a>
          </li>
        </ul>
        <div v-if="showMoreReposLink" class="tw-text-center">
          <div class="divider tw-my-0"/>
          <div class="ui borderless pagination menu narrow tw-my-2">
            <a
              class="item navigation tw-py-1" :class="{'disabled': page === 1}"
              @click="changePage(1)" :title="textFirstPage"
            >
              <svg-icon name="gitea-double-chevron-left" :size="16" class="tw-mr-1"/>
            </a>
            <a
              class="item navigation tw-py-1" :class="{'disabled': page === 1}"
              @click="changePage(page - 1)" :title="textPreviousPage"
            >
              <svg-icon name="octicon-chevron-left" :size="16" class="tw-mr-1"/>
            </a>
            <a class="active item tw-py-1">{{ page }}</a>
            <a
              class="item navigation" :class="{'disabled': page === finalPage}"
              @click="changePage(page + 1)" :title="textNextPage"
            >
              <svg-icon name="octicon-chevron-right" :size="16" class="tw-ml-1"/>
            </a>
            <a
              class="item navigation tw-py-1" :class="{'disabled': page === finalPage}"
              @click="changePage(finalPage)" :title="textLastPage"
            >
              <svg-icon name="gitea-double-chevron-right" :size="16" class="tw-ml-1"/>
            </a>
          </div>
        </div>
      </div>
    </div>
    <div v-if="!isOrganization" v-show="tab === 'organizations'" class="ui tab active list dashboard-orgs">
      <h4 class="ui top attached header tw-flex tw-items-center">
        <div class="tw-flex-1 tw-flex tw-items-center">
          {{ textMyOrgs }}
          <span class="ui grey label tw-ml-2">{{ organizationsTotalCount }}</span>
        </div>
        <a class="tw-flex tw-items-center muted" v-if="canCreateOrganization" :href="subUrl + '/org/create'" :data-tooltip-content="textNewOrg">
          <svg-icon name="octicon-plus"/>
        </a>
      </h4>
      <div v-if="!organizations.length" class="ui attached segment">
        <div class="empty-repo-or-org">
          <svg-icon name="octicon-organization" :size="24"/>
          <p>{{ textNoOrg }}</p>
        </div>
      </div>
      <div v-else class="ui attached table segment tw-rounded-b">
        <ul class="repo-owner-name-list">
          <li class="tw-flex tw-items-center tw-py-2" v-for="org in organizations" :key="org.name">
            <a class="repo-list-link muted" :href="subUrl + '/' + encodeURIComponent(org.name)">
              <svg-icon name="octicon-organization" :size="16" class="repo-list-icon"/>
              <div class="text truncate">{{ org.full_name ? `${org.full_name} (${org.name})` : org.name }}</div>
              <div><!-- div to prevent underline of label on hover -->
                <span class="ui tiny basic label" v-if="org.org_visibility !== 'public'">
                  {{ org.org_visibility === 'limited' ? textOrgVisibilityLimited: textOrgVisibilityPrivate }}
                </span>
              </div>
            </a>
            <div class="text light grey tw-flex tw-items-center tw-ml-2">
              {{ org.num_repos }}
              <svg-icon name="octicon-repo" :size="16" class="tw-ml-1 tw-mt-0.5"/>
            </div>
          </li>
        </ul>
      </div>
    </div>
  </div>
</template>
<style scoped>
ul {
  list-style: none;
  margin: 0;
  padding-left: 0;
}

ul li {
  padding: 0 10px;
}

ul li:not(:last-child) {
  border-bottom: 1px solid var(--color-secondary);
}

.repos-search {
  padding-bottom: 0 !important;
}

.repos-filter {
  margin-top: 0 !important;
  border-bottom-width: 0 !important;
}

.repos-filter .item {
  padding-left: 6px !important;
  padding-right: 6px !important;
}

.repo-list-link {
  min-width: 0; /* for text truncation */
  display: flex;
  align-items: center;
  flex: 1;
  gap: 0.5rem;
}

.repo-list-link .svg {
  color: var(--color-text-light-2);
}

.repo-list-icon {
  min-width: 16px;
  margin-right: 2px;
}

/* octicon-mirror has no padding inside the SVG */
.repo-list-icon.octicon-mirror {
  width: 14px;
  min-width: 14px;
  margin-left: 1px;
  margin-right: 3px;
}

.repo-owner-name-list li.active {
  background: var(--color-hover);
}

.empty-repo-or-org {
  margin-top: 1em;
  text-align: center;
  color: var(--color-placeholder-text);
}

.empty-repo-or-org p {
  margin: 1em auto;
}
</style>
