<script setup>

import { useI18n } from 'vue-i18n';
const i18n = useI18n();
const t = i18n.t;

import moment from 'moment';
import { ref, computed, useTemplateRef, onActivated, onDeactivated, inject, watch } from 'vue';
import { Button, TextInput, ProgressBar, InputDialog, SingleSelect } from '@cloudron/pankow';
import AppsModel from '../models/AppsModel.js';
import AppstoreModel from '../models/AppstoreModel.js';
import DomainsModel from '../models/DomainsModel.js';
import ApplinkDialog from '../components/ApplinkDialog.vue';
import AppInstallDialog from '../components/AppInstallDialog.vue';
import AppStoreItem from '../components/AppStoreItem.vue';
import CommunityAppDialog from '../components/CommunityAppDialog.vue';

const appsModel = AppsModel.create();
const appstoreModel = AppstoreModel.create();
const domainsModel = DomainsModel.create();

const ready = ref(false);
const apps = ref([]);
const search = ref('');
const domains = ref([]);
const addCustomAppMenu = ref([{
    label: 'App proxy',
    action: () => { window.location.href="/#/appstore/io.cloudron.builtin.appproxy"; }
  }, {
    label: 'Community app',
    action: () => { onInstallCommunityApp(); }
  }, {
    label: 'External link',
    action: () => { onAddAppLink(); }
  },
]);

// clear category on search
watch(search, (newValue) => {
  if (newValue) category.value ='';
});

function filterForNewApps(apps) {
    var minApps = apps.length < 12 ? apps.length : 12; // prevent endless loop
    var tmp = [];
    var i = 0;

    do {
        var offset = moment().subtract(i++, 'days');
        tmp = apps.filter(function (app) { return moment(app.publishedAt).isAfter(offset); });
    } while(tmp.length < minApps);

    return tmp;
}

const filteredApps = computed(() => {
  if (category.value) {
    if (category.value === 'new') {
      return filterForNewApps(apps.value);
    } else {
      return apps.value.filter(a => {
        const matchTags = categoryTagMap[category.value];
        if (!matchTags) return a.manifest.tags.some(tag => tag.toLowerCase() === category.value);
        return a.manifest.tags.some(tag => matchTags.includes(tag.toLowerCase()));
      });
    }
  }

  if (!search.value) return apps.value;

  const s = search.value.toLowerCase();

  return apps.value.filter(a => {
    if (a.manifest.title.toLowerCase().indexOf(s) !== -1) return true;
    if (a.manifest.tagline.toLowerCase().indexOf(s) !== -1) return true;
    if (a.manifest.tags.join().toLowerCase().indexOf(s) !== -1) return true;
    return false;
  });
});
const filteredAllApps = computed(() => {
  return filteredApps.value.filter(a => !a.featured).sort((a, b) => { return a.manifest.title.localeCompare(b.manifest.title); });
});
const filteredPopularApps = computed(() => {
  return filteredApps.value.filter(a => a.featured);
});
const appInstallDialog = useTemplateRef('appInstallDialog');
const searchInput = useTemplateRef('searchInput');
const inputDialog = useTemplateRef('inputDialog');
const features = inject('features');
const installedApps = ref([]);
const appstoreTokenError = ref(false);

const category = ref('');
const categories = [
  { id: '', label: t('appstore.category.all') },
  { id: 'new', label: t('appstore.category.newApps') },
  { id: 'ai', label: 'AI'},
  { id: 'analytics', label: 'Analytics'},
  { id: 'automation', label: 'Automation'},
  { id: 'blog', label: 'Blog'},
  { id: 'calendar', label: 'Calendar'},
  { id: 'chat', label: 'Chat'},
  { id: 'crm', label: 'CRM'},
  { id: 'document', label: 'Documents'},
  { id: 'email', label: 'Email'},
  { id: 'finance', label: 'Finance'},
  { id: 'forum', label: 'Forum'},
  { id: 'gallery', label: 'Gallery'},
  { id: 'game', label: 'Games'},
  { id: 'git', label: 'Code Hosting'},
  { id: 'hosting', label: 'Web Hosting'},
  { id: 'media', label: 'Media'},
  { id: 'no-code', label: 'No-code'},
  { id: 'notes', label: 'Notes'},
  { id: 'project', label: 'Project Management'},
  { id: 'rss', label: 'RSS'},
  { id: 'security', label: 'Security'},
  { id: 'sync', label: 'File Sync'},
  { id: 'voip', label: 'VoIP'},
  { id: 'wiki', label: 'Wiki'},
];

const categoryTagMap = {
  'ai': ['ai', 'ollama', 'chatgpt', 'llm', 'machine-learning', 'ai-assistant'],
  'analytics': ['analytics', 'tracker', 'monitoring', 'graphs', 'metrics', 'bi', 'tableau',
    'graphite', 'profiling', 'tag manager', 'visualization', 'data', 'statistics', 'status',
    'tracking'],
  'automation': ['automation', 'scheduling', 'cron', 'zapier', 'homeautomation',
    'home', 'assistant'],
  'blog': ['blog', 'weblog', 'ghost', 'wordpress', 'comments'],
  'calendar': ['calendar', 'calendars', 'caldav', 'carddav', 'appointment', 'appointments',
    'events', 'schedule', 'scheduler', 'doodle', 'calendly', 'ics', 'groupware', 'contacts',
    'addressbook', 'meetings'],
  'chat': ['chat', 'webchat', 'slack', 'gitter', 'teams', 'messaging',
    'instant-messaging', 'livechat', 'chat-widget', 'irc', 'riot', 'matrix',
    'zulip', 'conversation', 'communication', 'social'],
  'crm': ['crm', 'salesforce', 'sugarcrm', 'suitecrm', 'prm', 'erp',
    'pipedrive', 'customer support', 'helpdesk', 'zendesk', 'helpscout',
    'support', 'tickets', 'ticketing software', 'help', 'service management',
    'dolibarr'],
  'document': ['document', 'documents', 'docs', 'office', 'office365',
    'googledocs', 'editor', 'ocr', 'signature', 'docusign', 'pandadoc',
    'signning', 'collaboration', 'collaborative', 'digital', 'pdf',
    'draw', 'sketch', 'whiteboard', 'design', 'figma', 'prototyping',
    'writing', 'excel'],
  'email': ['email', 'mail', 'newsletter', 'campaign', 'mailchimp',
    'sendgrid', 'sendinblue', 'webmail', 'imap', 'smtp', 'gmail',
    'fastmail', 'marketing', 'campaigns', 'listmonk'],
  'finance': ['finance', 'finances', 'accounting', 'money', 'invoices',
    'mint', 'gnucash', 'invoice', 'expense', 'quote', 'expences',
    'firefly', 'firefly-iii', 'actual', 'control', 'track',
    'shop', 'inventory'],
  'forum': ['forum', 'community', 'discourse', 'bb', 'phpbb', 'vanilla',
    'stackoverflow', 'q&a platform', 'feedback', 'feature requests',
    'canny', 'portal', 'userresponse', 'uservoice'],
  'gallery': ['gallery', 'photo', 'pictures', 'images', 'picasa',
    'photos', 'instagram', 'flickr', 'imagebin', 'screencloud'],
  'game': ['game', 'games', 'gaming', 'multiplayer'],
  'git': ['version control', 'git', 'code hosting', 'code', 'development',
    'github', 'bitbucket', 'gitlab', 'gitea', 'ci', 'cd', 'docker',
    'registry', 'harbor', 'devtools', 'build', 'npm', 'repository',
    'artifactory', 'devops', 'drone', 'actions', 'bash', 'powershell',
    'golang', 'rails', 'vuejs', 'paste', 'pastebin'],
  'hosting': ['hosting', 'fileserver', 'webserver', 'server', 'cms',
    'static', 'website', 'jekyll', 'pages', 'netlify', 'lamp', 'stacks',
    'apache', 'php', 'squarespace', 'wix', 'mysql', 'proxy', 'external',
    'heritage'],
  'media': ['media', 'streaming', 'video', 'audio', 'movies',
    'mediacenter', 'plex', 'netflix', 'music', 'subsonic', 'spotify',
    'last.fm', 'mp3', 'music player', 'podcast', 'audiobook', 'videos',
    'youtube', 'vimeo', 'rtmp', 'obs', 'livestream', 'broadcast',
    'stream', 'media server', 'books', 'ebook', 'ebooks', 'epub', 'mobi',
    'calibre', 'kindle', 'kobo', 'opds', 'comics', 'manga', 'goodreads',
    'gpodder', 'torrent', 'bittorrent', 'qbittorrent', 'vuetorrent'],
  'no-code': ['no-code', 'nocode', 'airtable', 'spreadsheet', 'database',
    'graphql', 'typeform', 'api', 'contentful', 'strapi', 'sql',
    'applications', 'automations', 'dashboards', 'collaborate',
    'survey', 'surveymonkey', 'qualtrics', 'polls', 'forms', 'chatbot',
    'converter', 'tools'],
  'notes': ['notes', 'personal', 'evernote', 'memo', 'keep', 'onenote',
    'bookmarks', 'todo', 'diary', 'markdown', 'bookmark',
    'bookmark-manager', 'ideas', 'feed', 'productivity',
    'archive', 'readlater', 'readability', 'pocket', 'instapaper',
    'delicous', 'recipe', 'cookbook', 'cooking', 'food', 'meal-planner',
    'household'],
  'project': ['project', 'management', 'kanban', 'task management',
    'trello', 'asana', 'jira', 'basecamp', 'agile', 'scrum', 'gantt',
    'timeline', 'backlog', 'planning', 'organize', 'tasks', 'notion',
    'project management', 'bug', 'issue', 'srints', 'wekan',
    'time', 'clockify', 'harvest', 'toggl', 'asset', 'device', 'it',
    'mdm', 'license tracking', 'assets management', 'software audit'],
  'rss': ['rss', 'atom', 'reader', 'greader', 'feedly', 'news',
    'news feeds', 'feeds', 'bridge'],
  'security': ['password', 'bitwarden', 'vaultwarden', 'lastpass',
    '1password', 'manager', 'encryption', 'key', 'secret', 'vault',
    'hashicorp', 'auth', 'sso', 'oidc', 'openid', 'saml', '2fa',
    'two-factor authentication', 'dns', 'adblock', 'pihole', 'ublock',
    'privacy', 'vpn', 'openvpn', 'network', 'wireguard',
    'keycloak'],
  'sync': ['sync', 'file sharing', 'files', 'dropbox', 'cloud', 'file',
    'sharing', 'storage', 's3', 'objectstore', 'sftp', 'ftp', 'webdav',
    'file browser', 'airdrop', 'filesharing', 'filemanager', 'share'],
  'voip': ['voip', 'voice', 'conference', 'zoom', 'call', 'webrtc',
    'meeting', 'web meeting', 'p2p', 'sfu', 'bbb', 'im', 'videochat'],
  'wiki': ['wiki', 'confluence', 'sharepoint', 'knowledgebase',
    'knowledge base'],
};

async function onAppInstallDialogClose() {
  window.location.href = '#/appstore';
}

function onInstall(app) {
  window.location.href = `#/appstore/${app.manifest.id}?version=${app.manifest.version}`;
}

async function getAppList() {
  const [error, result] = await appstoreModel.list();
  if (error) {
    if (error.status === 402) return appstoreTokenError.value = true;
    return console.error(error);
  }

  apps.value = result;
}

async function getInstalledApps() {
  const [error, result] = await appsModel.list();
  if (error) return console.error(error);

  installedApps.value = result;
}

async function onHashChange() {
  if (window.location.hash.indexOf('#/appstore') !== 0) return;

  if (!appInstallDialog.value) return;

  const query = window.location.hash.slice('#/appstore/'.length);
  if (query) {
    const appId = query.split('?')[0];
    const params = new URLSearchParams(window.location.hash.slice(window.location.hash.indexOf('?')));
    const version = params.get('version') || 'latest';

    const [error, result] = await appstoreModel.get(appId, version);
    if (error || !result.manifest) {
      if (error) console.error(error);
      return inputDialog.value.info({
        title: t('appstore.appNotFoundDialog.title'),
        message: t('appstore.appNotFoundDialog.description', { appId, version }),
        confirmLabel: t('main.dialog.close'),
      });
    }

    const packageData = {
      ...result, // { id, creationDate, publishState, manifest, iconUrl }
      appStoreId: `${appId}@${version}`
    };
    appInstallDialog.value.open(packageData, installedApps.value.length >= features.value.appMaxCount, domains.value);
  }
}

const view = useTemplateRef('view');
const itemWidth = ref('unset');

function setItemWidth() {
  if (!view.value) return;

  const width = view.value.offsetWidth;
  const gap = 20; // flexbox gap value

  if (width <= 575) itemWidth.value = '100%';
  else if (width <= 800) itemWidth.value = Number((width-gap*2)/2).toFixed() + 'px';
  else if (width <= 1024) itemWidth.value = Number((width-gap*3)/3).toFixed() + 'px';
  else itemWidth.value = Number((width-gap*4)/4).toFixed() + 'px';
}

async function getDomains() {
  const [error, result] = await domainsModel.list();
  if (error) return console.error(error);

  domains.value = result;
}
const applinkDialog = useTemplateRef('applinkDialog');
const communityAppDialog = useTemplateRef('communityAppDialog');

function onAddAppLink() {
  applinkDialog.value.open();
}

function onApplinkAdded() {
  window.location.href = '#/apps';
}

function onInstallCommunityApp() {
  communityAppDialog.value.open();
}

function onCommunityAppSuccess(packageData) {
  appInstallDialog.value.open(packageData, installedApps.value.length >= features.value.appMaxCount, domains.value);
}

onActivated(async () => {
  setItemWidth();

  // only wait for applisting if we don't have one yet
  if (apps.value.length) getAppList();
  else await getAppList();

  ready.value = true;

  await getDomains();
  await getInstalledApps();

  // only deals with #/appstore/ hashes
  window.addEventListener('hashchange', onHashChange);
  onHashChange();

  window.addEventListener('resize', setItemWidth);

  if (window.innerWidth > 575) setTimeout(() => searchInput.value.focus(), 0);
});

onDeactivated(() => {
  search.value = '';
  category.value = '';

  window.removeEventListener('hashchange', onHashChange);
  window.removeEventListener('resize', setItemWidth);
});

</script>

<template>
  <div ref="view" class="content-large" style="width: 100%; height: 100%;">
    <InputDialog ref="inputDialog"/>
    <ApplinkDialog ref="applinkDialog" @success="onApplinkAdded"/>
    <CommunityAppDialog ref="communityAppDialog" @success="onCommunityAppSuccess"/>
    <AppInstallDialog ref="appInstallDialog" @close="onAppInstallDialogClose"/>

    <div class="filter-bar">
      <SingleSelect v-model="category" :options="categories" option-key="id" option-label="label" :disabled="!ready"/>
      <TextInput ref="searchInput" @keydown.esc="search = ''" v-model="search" :disabled="!ready" :placeholder="$t('appstore.searchPlaceholder')" style="flex-grow: 1;" autocomplete="off"/>
      <Button tool :menu="addCustomAppMenu">{{ $t('appstore.action.addCustomApp') }} </Button>
    </div>

    <div v-if="!ready" style="margin-top: 15px">
      <ProgressBar mode="indeterminate" :show-label="false" :slim="true" :show-track="false"/>
    </div>
    <div v-else-if="appstoreTokenError">
      Cloudron not registered. Reset registration <a href="#/cloudron-account">here</a>.
    </div>
    <div v-else>
      <div v-if="!search">
        <h2 v-show="filteredPopularApps.length">{{ $t('appstore.category.popular') }}</h2>
        <div class="grid">
          <AppStoreItem :style="{ width: itemWidth }" v-for="app in filteredPopularApps" :app="app" :key="app.id" :ref="'item-' + app.id" @click="onInstall(app)"/>
        </div>

        <h2 v-show="filteredAllApps.length">{{ $t('appstore.category.all') }}</h2>
        <div class="grid">
          <AppStoreItem :style="{ width: itemWidth }" v-for="app in filteredAllApps" :app="app" :key="app.id" :ref="'item-' + app.id" @click="onInstall(app)"/>
        </div>
      </div>
      <div v-else style="margin-top: 20px">
        <div class="grid">
          <AppStoreItem :style="{ width: itemWidth }" v-for="app in filteredApps" :app="app" :key="app.id" :ref="'item-' + app.id" @click="onInstall(app)"/>
        </div>
      </div>
    </div>
  </div>
</template>

<style scoped>

.grid-animation-move,
.grid-animation-enter-active,
.grid-animation-leave-active {
  transition: all 0.2s ease;
}

.grid-animation-enter-from {
  opacity: 0;
  transform: scale(0);
}

.grid-animation-leave-to {
  opacity: 0;
  transform: scale(0);
}

.filter-bar {
  width: 100%;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  justify-content: space-between;
}

.grid {
  position: relative;
  display: flex;
  gap: 20px;
  flex-wrap: wrap;
}

</style>
