import { inject, injectable } from '@theia/core/shared/inversify';
import { URI as Uri } from 'vscode-uri';
import URI from '@theia/core/lib/common/uri';
import { Deferred } from '@theia/core/lib/common/promise-util';
import {
  FileSystemProvider,
  FileSystemProviderError,
  FileSystemProviderErrorCode,
} from '@theia/filesystem/lib/common/files';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { DelegatingFileSystemProvider } from '@theia/filesystem/lib/common/delegating-file-system-provider';
import {
  FileService,
  FileServiceContribution,
} from '@theia/filesystem/lib/browser/file-service';
import { AuthenticationClientService } from '../auth/authentication-client-service';
import { AuthenticationSession } from '../../common/protocol/authentication-service';
import { ConfigService } from '../../common/protocol';

export namespace LocalCacheUri {
  export const scheme = 'arduino-local-cache';
  export const root = new URI(
    Uri.parse('/').with({ scheme, authority: 'create' })
  );
}

@injectable()
export class LocalCacheFsProvider
  implements FileServiceContribution, DelegatingFileSystemProvider.URIConverter
{
  @inject(ConfigService)
  protected readonly configService: ConfigService;

  @inject(AuthenticationClientService)
  protected readonly authenticationService: AuthenticationClientService;

  // TODO: do we need this? Cannot we `await` on the `init` call from `registerFileSystemProviders`?
  readonly ready = new Deferred<void>();

  private _localCacheRoot: URI;

  registerFileSystemProviders(fileService: FileService): void {
    fileService.onWillActivateFileSystemProvider(async (event) => {
      if (event.scheme === LocalCacheUri.scheme) {
        event.waitUntil(
          (async () => {
            this.init(fileService);
            const provider = await this.createProvider(fileService);
            fileService.registerProvider(LocalCacheUri.scheme, provider);
          })()
        );
      }
    });
  }

  to(resource: URI): URI | undefined {
    const relativePath = LocalCacheUri.root.relative(resource);
    if (relativePath) {
      return this.currentUserUri.resolve(relativePath).normalizePath();
    }
    return undefined;
  }

  from(resource: URI): URI | undefined {
    const relativePath = this.currentUserUri.relative(resource);
    if (relativePath) {
      return LocalCacheUri.root.resolve(relativePath);
    }
    return undefined;
  }

  protected async createProvider(
    fileService: FileService
  ): Promise<FileSystemProvider> {
    const delegate = await fileService.activateProvider('file');
    await this.ready.promise;
    return new DelegatingFileSystemProvider(
      delegate,
      {
        uriConverter: this,
      },
      new DisposableCollection(
        delegate.watch(this.localCacheRoot, {
          excludes: [],
          recursive: true,
        })
      )
    );
  }

  protected async init(fileService: FileService): Promise<void> {
    const config = await this.configService.getConfiguration();
    this._localCacheRoot = new URI(config.dataDirUri);
    for (const segment of ['RemoteSketchbook', 'ArduinoCloud']) {
      this._localCacheRoot = this._localCacheRoot.resolve(segment);
      await fileService.createFolder(this._localCacheRoot);
    }
    this.session(fileService).then(() => this.ready.resolve());
    this.authenticationService.onSessionDidChange(async (session) => {
      if (session) {
        await this.ensureExists(session, fileService);
      }
    });
  }

  public get currentUserUri(): URI {
    const { session } = this.authenticationService;
    if (!session) {
      throw new FileSystemProviderError(
        'Not logged in.',
        FileSystemProviderErrorCode.NoPermissions
      );
    }
    return this.toUri(session);
  }

  private get localCacheRoot(): URI {
    return this._localCacheRoot;
  }

  private async session(
    fileService: FileService
  ): Promise<AuthenticationSession> {
    return new Promise<AuthenticationSession>(async (resolve) => {
      const { session } = this.authenticationService;
      if (session) {
        await this.ensureExists(session, fileService);
        resolve(session);
        return;
      }
      const toDispose = new DisposableCollection();
      toDispose.push(
        this.authenticationService.onSessionDidChange(async (session) => {
          if (session) {
            await this.ensureExists(session, fileService);
            toDispose.dispose();
            resolve(session);
          }
        })
      );
    });
  }

  private async ensureExists(
    session: AuthenticationSession,
    fileService: FileService
  ): Promise<URI> {
    const uri = this.toUri(session);
    const exists = await fileService.exists(uri);
    if (!exists) {
      await fileService.createFolder(uri);
    }
    return uri;
  }

  private toUri(session: AuthenticationSession): URI {
    // Hack: instead of getting the UUID only, we get `auth0|UUID` after the authentication. `|` cannot be part of filesystem path or filename.
    return this._localCacheRoot.resolve(session.id.split('|')[1]);
  }
}
