import LinkHeader from 'http-link-header';
import { Quad } from 'rdf-js';
import { Reference } from '.';
import { get } from './pod';
import { TripleSubject, initialiseSubject } from './subject';
import { turtleToTriples } from './turtle';
import { initialiseDataset, Dataset } from './n3dataset';
import { instantiateFullTripleDocument } from './document/stored';
import { instantiateLocalTripleDocument } from './document/local';
import { instantiateLocalTripleDocumentForContainer } from './document/localForContainer';

/**
 * @ignore This is documented on use.
 */
export interface NewSubjectOptions {
  identifier?: string;
  identifierPrefix?: string;
};
/**
 * Methods that are shared by Documents in every state.
 *
 * Note that this does not include the `.save()` method, because that method is implemented
 * separately for every Document state.
 *
 * @ignore For internal use only, to combine with the other Document types.
 */
export interface BareTripleDocument {
  /**
   * Add a Subject — note that it is not written to the Pod until you call [[save]].
   *
   * @param addSubject.options By default, Tripledoc will automatically generate an identifier with
   *                           which this Subject can be identified within the Document, and which
   *                           is likely to be unique. The `options` parameter has a number of
   *                           optional properties. The first, `identifier`, takes a string. If set,
   *                           Tripledoc will not automatically generate an identifier. Instead, the
   *                           value of this parameter will be used as the Subject's identifier.
   *                           The second optional parameter, `identifierPrefix`, is also a string.
   *                           If set, it will be prepended before this Subject's identifier,
   *                           whether that's autogenerated or not.
   * @returns A [[TripleSubject]] instance that can be used to define its properties.
   */
  addSubject: (options?: NewSubjectOptions) => TripleSubject;
};

/**
 * An initialised Document that has not been stored in a Pod yet, and has no known location.
 *
 * You will obtain a LocalTripleDocumentForContainer when calling [[createDocumentInContainer]]. It
 * differs from a regular [[TripleDocument]] in that methods like [[TripleDocument.asRef]] are not
 * available, because the Reference for this Document is not known yet. When you [[save]] this
 * Document to the Pod, you will get a fully initialised [[TripleDocument]] as a return value.
 */
export interface LocalTripleDocumentForContainer extends BareTripleDocument {
  /**
   * Persist Subjects in this Document to the Pod.
   *
   * @param save.subjects Optional array of specific Subjects within this Document that should be
   *                      written to the Pod, i.e. excluding Subjects not in this array.
   * @return The updated Document with persisted Subjects.
   */
  save: (subjects?: TripleSubject[]) => Promise<TripleDocument>;
};

/**
 * An initialised Document that has not been stored to the Pod yet, but whose desired location is already known.
 *
 * This will be obtained when you call [[createDocument]]. Compared to a fully initialised
 * [[TripleDocument]], some methods relating to manipulating existing values on the Pod are not
 * available yet. They will be available on the [[TripleDocument]] returned when you call [[save]].
 */
export interface LocalTripleDocumentWithRef extends LocalTripleDocumentForContainer {
  /**
   * @returns The IRI of this Document.
   */
  asRef: () => Reference;
  /**
   * @ignore Deprecated.
   * @deprecated Replaced by [[asRef]].
   */
  asNodeRef: () => Reference;
};

/**
 * @ignore Not yet a supported API.
 */
export function hasRef(document: BareTripleDocument): document is LocalTripleDocumentWithRef {
  return typeof (document as LocalTripleDocumentWithRef).asRef === 'function';
}

/**
 * Local representation of a Document in a Pod.
 *
 * A TripleDocument gives you access to the values in the respective Document located on a Pod. They
 * can be accessed as [[TripleSubject]]s, which will allow you to manipulate their properties using
 * its `get*`, `add*`, `set*` and `remove*` methods — these changes will be applied to the Pod when
 * you call [[save]] on this Document.
 *
 * Note that these changes can not be _read_ from this TripleDocument; they will be available
 * on the TripleDocument that is returned when you call [[save]].
 */
export interface TripleDocument extends LocalTripleDocumentWithRef {
  /**
   * Remove a Subject - note that it is not removed from the Pod until you call [[save]].
   *
   * @param removeSubject.subject The IRI of the Subject to remove.
   */
  removeSubject: (subject: Reference) => void;
  /**
   * Find a Subject which has the value of `objectRef` for the Predicate `predicateRef`.
   *
   * @param findSubject.predicateRef The Predicate that must match for the desired Subject.
   * @param findSubject.objectRef The Object that must match for the desired Subject.
   * @returns `null` if no Subject matching `predicateRef` and `objectRef` is found,
   *          a random one of the matching Subjects otherwise.
   */
  findSubject: (predicateRef: Reference, objectRef: Reference) => TripleSubject | null;
  /**
   * Find Subjects which have the value of `objectRef` for the Predicate `predicateRef`.
   *
   * @param findSubjects.predicateRef - The Predicate that must match for the desired Subjects.
   * @param findSubjects.objectRef - The Object that must match for the desired Subjects.
   * @returns An array with every matching Subject, and an empty array if none match.
   */
  findSubjects: (predicateRef: Reference, objectRef: Reference) => TripleSubject[];
  /**
   * Given the IRI of a Subject, return an instantiated [[TripleSubject]] representing its values.
   *
   * @param getSubject.subjectRef IRI of the Subject to inspect.
   * @returns Instantiation of the Subject at `subjectRef`, ready for inspection.
   */
  getSubject: (subjectRef: Reference) => TripleSubject;
  /**
   * Get all Subjects in this Document
   *
   * @returns All Subjects in this Document that are of the given type.
   * @ignore Experimental API.
   */
  experimental_getAllSubjects: () => TripleSubject[];
  /**
   * @ignore Deprecated
   * @deprecated Replaced by getAllSubjectsOfType
   */
  getSubjectsOfType: (typeRef: Reference) => TripleSubject[];
  /**
   * Get all Subjects in this Document of a given type.
   *
   * @param getAllSubjectsOfType.typeRef IRI of the type the desired Subjects should be of.
   * @returns All Subjects in this Document that are of the given type.
   */
  getAllSubjectsOfType: (typeRef: Reference) => TripleSubject[];
  /**
   * @ignore Experimental API, might change in the future to return an instantiated Document
   * @deprecated Replaced by [[getAclRef]]
   */
  getAcl: () => Reference | null;
  /**
   * @ignore Experimental API, might change in the future to return an instantiated Document
   */
  getAclRef: () => Reference | null;
  /**
   * @ignore Experimental API, will probably change as the Solid specification changes to no longer support WebSockets
   */
  getWebSocketRef: () => Reference | null;
  /**
   * @deprecated
   * @ignore This is mostly a convenience function to make it easy to work with n3 and tripledoc
   *         simultaneously. If you rely on this, it's probably best to either file an issue
   *         describing what you want to do that Tripledoc can't do directly, or to just use n3
   *         directly.
   * @returns An RDF/JS Dataset containing the Triples pertaining to this Document that are stored
   *          on the user's Pod. Note that this does not contain Triples that have not been saved
   *          yet - those can be retrieved from the respective [[TripleSubject]]s.
   */
  getStore: () => Dataset;
  /**
   * @deprecated
   * @ignore This is mostly a convenience function to make it easy to work with n3 and tripledoc
   *         simultaneously. If you rely on this, it's probably best to either file an issue
   *         describing what you want to do that Tripledoc can't do directly, or to just use n3
   *         directly.
   * @returns The Triples pertaining to this Document that are stored on the user's Pod. Note that
   *          this does not return Triples that have not been saved yet - those can be retrieved
   *          from the respective [[TripleSubject]]s.
   */
  getTriples: () => Quad[];
  /**
   * @deprecated Replaced by [[getTriples]]
   */
  getStatements: () => Quad[];
};

/**
 * @ignore Not yet a supported API.
 */
export function isSavedToPod(document: BareTripleDocument): document is TripleDocument {
  return typeof (document as TripleDocument).getTriples === 'function';
}

/**
 * Initialise a new Turtle document
 *
 * Note that this Document will not be created on the Pod until you call [[save]] on it.
 *
 * @param ref URL where this document should live
 */
export function createDocument(ref: Reference): LocalTripleDocumentWithRef {
  return instantiateDocument([], { documentRef: ref, existsOnPod: false });
}

/**
 * Initialise a new Turtle Document in a Container
 *
 * Note that this Document will not be created on the Pod until you call [[save]] on it.
 *
 * @param containerRef URL of the Container in which this document should live
 */
export function createDocumentInContainer(containerRef: Reference): LocalTripleDocumentForContainer {
  return instantiateDocument([], { containerRef: containerRef, existsOnPod: false });
}

/**
 * Retrieve a document containing RDF triples
 *
 * @param documentRef Where the document lives.
 * @returns Representation of triples in the document at `uri`.
 */
export async function fetchDocument(uri: Reference): Promise<TripleDocument> {
  // Remove fragment identifiers (e.g. `#me`) from the URI:
  const docUrl = new URL(uri);
  const documentRef: Reference = docUrl.origin + docUrl.pathname + docUrl.search;

  const response = await get(documentRef);
  if (response.ok === false) {
    throw new Error(`Fetching the Document failed: ${response.status} ${response.statusText}.`);
  }
  const rawDocument = await response.text();
  const triples = await turtleToTriples(rawDocument, documentRef);

  let aclRef: Reference | undefined = extractAclRef(response, documentRef);
  const webSocketRef: Reference | null = response.headers.get('Updates-Via');

  return instantiateDocument(
    triples,
    {
      aclRef: aclRef,
      documentRef: documentRef,
      webSocketRef: webSocketRef || undefined,
      existsOnPod: true,
    },
  );
}

/**
 * @internal
 */
export function extractAclRef(response: Response, documentRef: Reference) {
  let aclRef: Reference | undefined;
  const linkHeader = response.headers.get('Link');
  // `LinkHeader` might not be present when using the UMD build in the browser,
  // in which case we just don't parse the ACL header. It is recommended to use a non-UMD build
  // that supports code splitting anyway.
  if (linkHeader && LinkHeader) {
    const parsedLinks = LinkHeader.parse(linkHeader);
    const aclLinks = parsedLinks.get('rel', 'acl');
    if (aclLinks.length === 1) {
      aclRef = new URL(aclLinks[0].uri, documentRef).href;
    }
  }
  return aclRef;
}

type DocOrContainerMetadata = { documentRef: Reference } | { containerRef: Reference };
/**
 * @ignore For internal use only.
 */
export type DocumentMetadata = DocOrContainerMetadata & {
  aclRef?: Reference;
  webSocketRef?: Reference;
  existsOnPod?: boolean;
};
function hasKnownRef<Metadata extends DocumentMetadata>(metadata: Metadata): metadata is Metadata & { documentRef: Reference } {
  return typeof (metadata as { documentRef?: Reference }).documentRef === 'string';
}
function existsOnPod<Metadata extends DocumentMetadata>(metadata: Metadata): metadata is Metadata & { existsOnPod: true } {
  return (metadata as { existsOnPod?: boolean }).existsOnPod === true;
}
/**
 * @internal
 */
export function instantiateDocument(triples: Quad[], metadata: DocumentMetadata & {existsOnPod: true, documentRef: Reference}): TripleDocument;
export function instantiateDocument(triples: Quad[], metadata: DocumentMetadata & {documentRef: Reference}): LocalTripleDocumentWithRef;
export function instantiateDocument(triples: Quad[], metadata: DocumentMetadata): LocalTripleDocumentForContainer;
export function instantiateDocument(
  triples: Quad[],
  metadata: DocumentMetadata,
): LocalTripleDocumentForContainer | LocalTripleDocumentWithRef | TripleDocument {
  const dataset = initialiseDataset();
  dataset.addAll(triples);

  const subjectCache = initialiseSubjectCache();

  if (!hasKnownRef(metadata)) {
    return instantiateLocalTripleDocumentForContainer(dataset, subjectCache, metadata);
  }

  if (!existsOnPod(metadata)) {
    return instantiateLocalTripleDocument(dataset, subjectCache, metadata);
  }

  return instantiateFullTripleDocument(dataset, subjectCache, metadata);
}

/**
 * @internal
 */
export interface SubjectCache {
  getSubject: TripleDocument['getSubject'];
  setDocument: (document: BareTripleDocument) => void;
  getAccessedSubjects: () => { [iri: string]: TripleSubject };
};
function initialiseSubjectCache(): SubjectCache {
  let sourceDocument: BareTripleDocument;
  const accessedSubjects: { [iri: string]: TripleSubject } = {};

  const setDocument = (newDocument: BareTripleDocument) => {
    sourceDocument = newDocument;
  };

  const getSubject = (subjectRef: Reference) => {
    // Allow relative URLs to access Subjects if we know where the Document is:
    subjectRef = hasRef(sourceDocument)
      ? new URL(subjectRef, sourceDocument.asRef()).href
      : subjectRef;
    if (!accessedSubjects[subjectRef]) {
      accessedSubjects[subjectRef] = initialiseSubject(sourceDocument, subjectRef);
    }
    return accessedSubjects[subjectRef];
  };

  const getAccessedSubjects = () => accessedSubjects;

  return {
    getSubject,
    setDocument,
    getAccessedSubjects,
  };
}

/**
 * @internal
 */
export function instantiateBareTripleDocument(
  subjectCache: SubjectCache,
  metadata: DocumentMetadata,
): BareTripleDocument {
  const addSubject = (
    {
      identifier = generateIdentifier(),
      identifierPrefix = '',
    }: NewSubjectOptions = {},
  ) => {
    const subjectRef: Reference =
      (hasKnownRef(metadata) ? metadata.documentRef : '') + '#' + identifierPrefix + identifier;
    return subjectCache.getSubject(subjectRef);
  };

  const bareTripleDocument: BareTripleDocument = {
    addSubject: addSubject,
  };

  return bareTripleDocument;
}

/**
 * @internal
 */
export function getPendingChanges(
  subjects: TripleSubject[],
  document: BareTripleDocument,
  dataset: Dataset,
) {
  const relevantSubjects = subjects.filter((subject) => subject.getDocument() === document);
  type UpdateTriples = [Quad[], Quad[]];
  const [allDeletions, allAdditions] = relevantSubjects.reduce<UpdateTriples>(
    ([deletionsSoFar, additionsSoFar], subject) => {
      const [deletions, additions] = subject.getPendingTriples();
      return [deletionsSoFar.concat(deletions), additionsSoFar.concat(additions)];
    },
    [[], []],
  );

  let newTriples: Quad[] = dataset.toArray()
    .concat(allAdditions)
    .filter(tripleToDelete => allDeletions.findIndex((triple) => triple.equals(tripleToDelete)) === -1);

  return {
    allAdditions,
    allDeletions,
    newTriples,
  };
}

/**
 * Generate a string that can be used as the unique identifier for a Subject
 *
 * This function works by starting with a date string (so that Subjects can be
 * sorted chronologically), followed by a random number generated by taking a
 * random number between 0 and 1, and cutting off the `0.`.
 *
 * @internal
 * @returns An string that's likely to be unique
 */
const generateIdentifier = () => {
  return Date.now().toString() + Math.random().toString().substring('0.'.length);
}
