/*
 * Copyright 2018 The Data Transfer Project Authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.datatransferproject.transfer.solid.contacts;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import ezvcard.Ezvcard;
import ezvcard.VCard;
import ezvcard.parameter.EmailType;
import ezvcard.parameter.TelephoneType;
import ezvcard.property.Email;
import ezvcard.property.Note;
import ezvcard.property.Organization;
import ezvcard.property.Photo;
import ezvcard.property.StructuredName;
import ezvcard.property.Telephone;
import ezvcard.property.Url;
import org.apache.jena.rdf.model.Model;
import org.apache.jena.rdf.model.ModelFactory;
import org.apache.jena.rdf.model.Resource;
import org.apache.jena.vocabulary.DCTerms;
import org.apache.jena.vocabulary.DC_11;
import org.apache.jena.vocabulary.RDF;
import org.apache.jena.vocabulary.VCARD4;
import org.datatransferproject.spi.transfer.idempotentexecutor.IdempotentImportExecutor;
import org.datatransferproject.spi.transfer.provider.ImportResult;
import org.datatransferproject.spi.transfer.provider.Importer;
import org.datatransferproject.transfer.solid.SolidUtilities;
import org.datatransferproject.types.common.models.contacts.ContactsModelWrapper;
import org.datatransferproject.types.transfer.auth.CookiesAndUrlAuthData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;

import static com.google.common.base.Preconditions.checkState;

public class SolidContactsImport implements Importer<CookiesAndUrlAuthData, ContactsModelWrapper> {
  //See https://www.w3.org/TR/vcard-rdf/ for details.

  private static final Logger logger = LoggerFactory.getLogger(SolidContactsExport.class);
  private static final String TEST_SLUG_NAME = "ImportedAddressBook";
  private static final String BASE_DIRECTORY = "/inbox/";
  private static final String BASIC_CONTAINER_TYPE = "http://www.w3.org/ns/ldp#BasicContainer";
  private static final String BASIC_RESOURCE_TYPE = "http://www.w3.org/ns/ldp#Resource";


  @SuppressWarnings("deprecation")
  private static final ImmutableList<Resource> EMAIL_TYPE_RESOURCES = ImmutableList.of(
      VCARD4.Home,
      VCARD4.Work,
      // Deprecated Types:
      VCARD4.Dom,
      VCARD4.Internet,
      VCARD4.ISDN,
      VCARD4.Pref);
  private static final ImmutableMap<String, Resource> MAP_OF_EMAIL_TYPES;

  @SuppressWarnings("deprecation")
  private static final ImmutableList<Resource> PHONE_TYPE_RESOURCES = ImmutableList.of(
      VCARD4.Cell,
      VCARD4.Fax,
      VCARD4.Home,
      VCARD4.Pager,
      VCARD4.Pref,
      VCARD4.Text,
      VCARD4.TextPhone,
      VCARD4.Video,
      VCARD4.Voice,
      VCARD4.Work);
  private static final ImmutableMap<String, Resource> MAP_OF_PHONE_TYPES;


  static {
    MAP_OF_EMAIL_TYPES = ImmutableMap.copyOf(EMAIL_TYPE_RESOURCES.stream()
        .collect(Collectors.toMap(
            r -> r.getLocalName().toLowerCase(),
            Function.identity()
    )));

    MAP_OF_PHONE_TYPES = ImmutableMap.copyOf(PHONE_TYPE_RESOURCES.stream()
        .collect(Collectors.toMap(
            r -> r.getLocalName().toLowerCase(),
            Function.identity()
        )));
  }

  @VisibleForTesting
  static final String IMPORTED_ADDRESS_BOOK_PATH = BASE_DIRECTORY + TEST_SLUG_NAME + "/";

  @Override
  public ImportResult importItem(
      UUID jobId,
      IdempotentImportExecutor idempotentExecutor,
      CookiesAndUrlAuthData authData,
      ContactsModelWrapper data) throws Exception {
    checkState(authData.getCookies().size() == 1,
        "Exactly 1 cookie expected: %s",
        authData.getCookies());

    SolidUtilities solidUtilities = new SolidUtilities(authData.getCookies().get(0));

    String url = authData.getUrl();

    List<VCard> vcards = Ezvcard.parse(data.getVCards()).all();
    createContent(idempotentExecutor, url, vcards, solidUtilities);
    return ImportResult.OK;
  }

  private void createContent(
      IdempotentImportExecutor idempotentExecutor,
      String baseUrl,
      List<VCard> people,
      SolidUtilities utilities)
      throws Exception {
    String addressBookSlug = TEST_SLUG_NAME;

    String containerUrl = createContainer(baseUrl + BASE_DIRECTORY, addressBookSlug, utilities);

    idempotentExecutor.executeOrThrowException(baseUrl + containerUrl,
        addressBookSlug,
        () -> createIndex(baseUrl + containerUrl, addressBookSlug, utilities));

    String personDirectory = idempotentExecutor.executeOrThrowException(
        baseUrl + containerUrl + "person",
        addressBookSlug,
        () -> createPersonDirectory(baseUrl + containerUrl, utilities));

    Map<String, VCard> insertedPeople = new HashMap<>();
    for (VCard person : people ){
      insertedPeople.put(
        importPerson(idempotentExecutor, person, baseUrl, personDirectory, utilities),
        person);
    }

    // people.stream()
    //     .collect(Collectors.toMap(
    //         p -> importPerson(idempotentExecutor, p, baseUrl, personDirectory, utilities),
    //         Function.identity()));

    idempotentExecutor.executeOrThrowException(
        "peopleFile",
        addressBookSlug,
        () -> createPeopleFile(baseUrl, containerUrl, insertedPeople, utilities));
  }

  private String importPerson(IdempotentImportExecutor executor,
      VCard person,
      String baseUrl,
      String personDirectory,
      SolidUtilities utilities) throws Exception {
    return executor.executeAndSwallowIOExceptions(
        Integer.toString(person.hashCode()),
        person.getFormattedName().getValue(),
        () -> insertPerson(baseUrl, personDirectory, person, utilities));
  }

  private String createContainer(String url, String slug, SolidUtilities utilities) throws Exception {
    Model containerModel = ModelFactory.createDefaultModel();
    Resource containerResource = containerModel.createResource("");
    containerResource.addProperty(DCTerms.title, slug);
    return utilities.postContent(url, slug, BASIC_CONTAINER_TYPE, containerModel);
  }

  private String createIndex(String url, String slug, SolidUtilities utilities) throws Exception {
    Model model = ModelFactory.createDefaultModel();
    Resource containerResource = model.createResource("#this");
    containerResource.addProperty(RDF.type, model.getResource(VCARD4.NS + "AddressBook"));
    containerResource.addProperty(
        model.createProperty(VCARD4.NS + "nameEmailIndex"),
        model.createResource("people.ttl"));
    containerResource.addProperty(
        model.createProperty(VCARD4.NS + "groupIndex"),
        model.createResource("groups.ttl"));
    containerResource.addProperty(DC_11.title, slug);
    return utilities.postContent(
        url,
        "index",
        BASIC_RESOURCE_TYPE,
        model);
  }

  private String createPersonDirectory(String url, SolidUtilities utilities) throws IOException {
    Model personDirectoryModel = ModelFactory.createDefaultModel();
    personDirectoryModel.createResource("");
    return utilities.postContent(
        url,
        "Person",
        BASIC_CONTAINER_TYPE,
        personDirectoryModel);
  }

  private String insertPerson(String baseUrl, String container, VCard person,  SolidUtilities utilities) {
    Model personContainerModel = ModelFactory.createDefaultModel();
    personContainerModel.createResource("");
    try {
      String directory = utilities.postContent(
          baseUrl + container,
          null,
          BASIC_CONTAINER_TYPE,
          personContainerModel);

      return utilities.postContent(
          baseUrl + directory,
          "index",
          BASIC_RESOURCE_TYPE,
          getPersonModel(person));
    } catch (IOException e) {
      throw new IllegalStateException("Couldn't insert: " + person.getFormattedName()
          + " into: " + baseUrl + container, e);
    }
  }

  private String createPeopleFile(
      String baseUrl,
      String containerUrl,
      Map<String, VCard> importedPeople,
      SolidUtilities utilities) throws Exception {
    Model peopleModel = ModelFactory.createDefaultModel();
    Resource indexResource = peopleModel.createResource("index.ttl#this");

    for (String insertedId : importedPeople.keySet()) {

      VCard insertedPerson = importedPeople.get(insertedId);
      String relativePath = insertedId.replace(containerUrl, "");

      Resource personResource = peopleModel.createResource(relativePath + "#this");
      if (insertedPerson.getFormattedName() != null) {
        personResource.addProperty(VCARD4.fn, insertedPerson.getFormattedName().getValue());
      }
      personResource.addProperty(
          peopleModel.createProperty(VCARD4.NS, "inAddressBook"),
          indexResource);
    }

    return utilities.postContent(
        baseUrl + containerUrl,
        "people",
        BASIC_RESOURCE_TYPE,
        peopleModel);
  }


  @VisibleForTesting
  final static Model getPersonModel(VCard vcard) {
    Model personModel = ModelFactory.createDefaultModel();
    Resource r = personModel.createResource("#this");
    r.addProperty(RDF.type, VCARD4.Individual);

    if (null != vcard.getFormattedName()) {
      r.addProperty(VCARD4.fn, vcard.getFormattedName().getValue());
    }

    for (StructuredName structuredName : vcard.getStructuredNames()) {
      Resource strucName = personModel.createResource();
      if (!Strings.isNullOrEmpty(structuredName.getFamily())) {
        strucName.addProperty(VCARD4.family_name, structuredName.getFamily());
      }
      if (!Strings.isNullOrEmpty(structuredName.getGiven())) {
        strucName.addProperty(VCARD4.given_name, structuredName.getGiven());
      }

      structuredName.getPrefixes()
          .forEach(prefix -> strucName.addProperty(VCARD4.hasHonorificPrefix, prefix));

      structuredName.getSuffixes()
          .forEach(suffix -> strucName.addProperty(VCARD4.hasHonorificSuffix, suffix));

      structuredName.getAdditionalNames()
          .forEach(additional -> strucName.addProperty(VCARD4.hasAdditionalName, additional));

      r.addProperty(VCARD4.hasName, strucName);
    }

    for (Email email : vcard.getEmails()) {
      String mailTo = "mailto:" + email.getValue();
      if (email.getTypes().isEmpty()) {
        r.addProperty(VCARD4.hasEmail, mailTo);
      } else {
        Resource emailResource = personModel.createResource();
        emailResource.addProperty(VCARD4.value, mailTo);
        for (EmailType type : email.getTypes()) {
          for (Resource emailTypeResource :
              getPhoneOrMailTypes(type.getValue(), MAP_OF_EMAIL_TYPES)) {
            emailResource.addProperty(RDF.type, emailTypeResource);
          }
        }
        r.addProperty(VCARD4.hasEmail, emailResource);
      }
    }

    for (Telephone telephone : vcard.getTelephoneNumbers()) {
      if (telephone.getTypes().isEmpty()) {
        r.addProperty(VCARD4.hasTelephone, telephone.getText());
      } else {
        Resource telephoneResource = personModel.createResource();
        telephoneResource.addProperty(VCARD4.value, telephone.getText());
        for (TelephoneType type : telephone.getTypes()) {
          for (Resource telTypeResource : getPhoneOrMailTypes(type.getValue(), MAP_OF_PHONE_TYPES))
            telephoneResource.addProperty(RDF.type, telTypeResource);
        }
        r.addProperty(VCARD4.hasTelephone, telephoneResource);
      }
    }

    if (vcard.getOrganization() != null) {
      r.addProperty(VCARD4.organization_name, vcard.getOrganization().getValues().get(0));
    }

    for (Organization organization : vcard.getOrganizations()) {
      organization.getValues().stream().forEach(
          v -> r.addProperty(VCARD4.hasOrganizationName, v));
    }

    for (Url url : vcard.getUrls()) {
      r.addProperty(VCARD4.hasURL, url.getValue());
    }

    for (Note note : vcard.getNotes()) {
      r.addProperty(VCARD4.hasNote, note.getValue());
    }

    for (Photo photo : vcard.getPhotos()) {
      r.addProperty(VCARD4.hasPhoto, photo.getUrl());
    }

    return personModel;
  }

  /** Looks up the {@link Resource}s for a given string, that might be comma delimited. **/
  private static ImmutableList<Resource> getPhoneOrMailTypes(
      String type,
      Map<String, Resource> map) {
    return ImmutableList.copyOf(
        Arrays.stream(type.split(",")).map(t -> {
          Resource r = map.get(t.toLowerCase());
          if (r == null) {
            logger.warn("%s didn't contain '%s' from %s", map, t.toLowerCase(), type);
            r = ModelFactory.createDefaultModel().getResource(VCARD4.NS + t);
          }
          return  r;
        })
    .collect(Collectors.toList()));
  }
}