/*
 * Copyright 2003-2016 JetBrains s.r.o.
 *
 * 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
 *
 * http://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 jetbrains.mps.persistence;

import jetbrains.mps.extapi.persistence.ModelFactoryService;
import jetbrains.mps.extapi.persistence.datasource.DataSourceFactoryFromURL;
import jetbrains.mps.extapi.persistence.datasource.DataSourceFactoryRuleCoreService;
import jetbrains.mps.extapi.persistence.datasource.PreinstalledURLDataSourceFactories;
import jetbrains.mps.extapi.persistence.datasource.URLNotSupportedException;
import jetbrains.mps.project.MPSExtentions;
import jetbrains.mps.util.FileUtil;
import jetbrains.mps.util.JDOMUtil;
import jetbrains.mps.vfs.IFile;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.persistence.DataSource;
import org.jetbrains.mps.openapi.persistence.DataSourceListener;
import org.jetbrains.mps.openapi.persistence.ModelFactory;
import org.jetbrains.mps.openapi.persistence.ModelSaveException;
import org.jetbrains.mps.openapi.persistence.MultiStreamDataSource;
import org.jetbrains.mps.openapi.persistence.StreamDataSource;
import org.jetbrains.mps.openapi.persistence.datasource.DataSourceType;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.util.LinkedHashMap;
import java.util.Map;

import static java.util.Collections.singletonMap;

/**
 * evgeny, 3/6/13
 */
public final class PersistenceUtil {
  private static final Logger LOG = LogManager.getLogger(PersistenceUtil.class);

  private PersistenceUtil() {
  }

  /**
   * Try to load a model using a default {@link org.jetbrains.mps.openapi.persistence.ModelFactory}
   * identified by <code>extension</code> from supplied textual <code>content</code>.
   *
   * @return <code>null</code> if fails to load model from the content supplied (either model read error, no model factory for the extension, or factory
   * doesn't support textual content)
   */
  @Nullable
  public static SModel loadModel(@NotNull String content) {
    @SuppressWarnings("ConstantConditions")
    @NotNull ModelFactory factory = ModelFactoryService.getInstance().getFactoryByType(PreinstalledModelFactoryTypes.PLAIN_XML);
    byte[] bytes = content.getBytes(FileUtil.DEFAULT_CHARSET);
    return loadModel(bytes, factory);
  }

  /**
   * Try to load a model using {@link org.jetbrains.mps.openapi.persistence.ModelFactory}
   * from supplied <code>content</code>.
   *
   * @return <code>null</code> if fails to load model from the content supplied (either model read error)
   *         or loaded model from the byte array content using the supplied model factory
   */
  @Nullable
  public static SModel loadModel(final byte[] content, @NotNull ModelFactory factory) {
    try {
      SModel model = factory.load(new ByteArrayInputSource(content), singletonMap(ModelFactory.OPTION_CONTENT_ONLY, Boolean.TRUE.toString()));
      model.load();
      return model;
    } catch (IOException ex) {
      return null;
    }
  }

  @Nullable
  public static SModel loadBinaryModel(final byte[] content) {
    //noinspection ConstantConditions
    return loadModel(content, ModelFactoryService.getInstance().getFactoryByType(PreinstalledModelFactoryTypes.BINARY));
  }

  public static SModel loadModel(@NotNull IFile file) {
    try {
      URL url = file.getUrl();
      DataSourceFactoryFromURL dataSourceFactory = getDataSourceFactory(url);
      if (dataSourceFactory == null) {
        return null;
      }
      DataSource dataSource = dataSourceFactory.create(url, null);
      if (dataSource.getType() == null) {
        return null;
      }
      ModelFactory factory = ModelFactoryService.getInstance().getDefaultModelFactory(dataSource.getType());
      if (factory == null) {
        return null;
      }
      final Map<String, String> options = singletonMap(ModelFactory.OPTION_CONTENT_ONLY, Boolean.TRUE.toString());
      SModel model = factory.load(dataSource, options);
      model.load();
      return model;
    } catch (IOException | URLNotSupportedException e) {
      LOG.error("", e);
      return null;
    }
  }

  @Nullable
  private static DataSourceFactoryFromURL getDataSourceFactory(@NotNull URL url) {
    DataSourceFactoryRuleCoreService service = DataSourceFactoryRuleCoreService.getInstance();
    DataSourceFactoryFromURL dataSourceFactory = service.getFactory(url);
    if (dataSourceFactory == null) {
      LOG.error("Data Source Factory is not found for " + url);
      return null;
    }
    return dataSourceFactory;
  }

  public static String saveModel(final SModel model, String extension) {
    ModelFactory factory = PersistenceRegistry.getInstance().getModelFactory(extension);
    if (factory == null || factory.isBinary()) {
      return null;
    }
    try {
      InMemoryStreamDataSource source = new InMemoryStreamDataSource();
      factory.save(model, source);
      return source.getContent(FileUtil.DEFAULT_CHARSET_NAME);
    } catch (ModelSaveException | IOException e) {
      LOG.error(e);
    }
    return null;
  }

  public static Element saveModelToXml(final SModel model) {
    try {
      SAXBuilder saxBuilder = new SAXBuilder();
      Element rootElement = saxBuilder.build(modelContentAsStream(model, MPSExtentions.MODEL)).getRootElement();
      rootElement.detach();
      return rootElement;
    } catch (IOException | JDOMException e) {
      LOG.error(e);
    }
    return null;
  }

  public static SModel loadModelFromXml(final Element element) {
    return loadModel(JDOMUtil.asString(new org.jdom.Document(element)));
  }

  public static byte[] saveBinaryModel(final SModel model) {
    ModelFactory factory = PersistenceRegistry.getInstance().getModelFactory(MPSExtentions.MODEL_BINARY);
    try {
      InMemoryStreamDataSource source = new InMemoryStreamDataSource();
      factory.save(model, source);
      return source.myStream.toByteArray();
    } catch (ModelSaveException | IOException e) {
      LOG.error(e);
    }
    return null;
  }

  /**
   * Serialize model with a persistence identified by extension and provide access to serialized content through InputStream.
   * @return empty stream in case serialization failed. Caller is responsible to close the stream.
   */
  public static InputStream modelContentAsStream(final SModel model, String extension) {
    ModelFactory factory = PersistenceRegistry.getInstance().getModelFactory(extension);
    if (factory != null) {
      try {
        InMemoryStreamDataSource source = new InMemoryStreamDataSource();
        factory.save(model, source);
        return source.getContentAsStream();
      } catch (ModelSaveException | IOException e) {
        LOG.error(e);
        // fall-through
      }
    }
    return new ByteArrayInputStream(new byte[0]);
  }

  public static String savePerRootModel(final SModel model, String name) {
    ModelFactory factory = ModelFactoryService.getInstance().getFactoryByType(PreinstalledModelFactoryTypes.PER_ROOT_XML);
    if (factory == null || factory.isBinary()) {
      return null;
    }
    try {
      InMemoryMultiStreamDataSource source = new InMemoryMultiStreamDataSource();
      factory.save(model, source);
      return source.getContent(name, FileUtil.DEFAULT_CHARSET_NAME);
    } catch (ModelSaveException | IOException e) {
      LOG.error(e);
    }
    return null;
  }

  public static String savePerRootModel(final SModel model, boolean isHeader) {
    ModelFactory factory = ModelFactoryService.getInstance().getFactoryByType(PreinstalledModelFactoryTypes.PER_ROOT_XML);
    if (factory == null || factory.isBinary()) {
      return null;
    }
    try {
      InMemoryMultiStreamDataSource source = new InMemoryMultiStreamDataSource();
      factory.save(model, source);
      if (isHeader) {
        return source.getContent(MPSExtentions.DOT_MODEL_HEADER, FileUtil.DEFAULT_CHARSET_NAME);
      } else {
        for (String name : source.getAvailableStreams()) {
          if (name.equals(MPSExtentions.DOT_MODEL_HEADER)) continue;
          return source.getContent(name, FileUtil.DEFAULT_CHARSET_NAME);
        }
      }
    } catch (ModelSaveException | IOException e) {
      LOG.error(e);
    }
    return null;
  }

  public static abstract class StreamDataSourceBase implements StreamDataSource {
    @NotNull
    @Override
    public String getLocation() {
      return "in-memory";
    }

    @Override
    public boolean isReadOnly() {
      return true;
    }

    @Override
    public InputStream openInputStream() throws IOException {
      throw new UnsupportedOperationException();
    }

    @Override
    public OutputStream openOutputStream() throws IOException {
      throw new UnsupportedOperationException();
    }

    @Override
    public void addListener(@NotNull DataSourceListener listener) {

    }

    @Override
    public void removeListener(@NotNull DataSourceListener listener) {

    }

    @Override
    public long getTimestamp() {
      return 0;
    }
  }

  public static class InMemoryStreamDataSource extends StreamDataSourceBase {
    private ByteArrayOutputStream myStream;

    @Override
    public OutputStream openOutputStream() throws IOException {
      myStream = new ByteArrayOutputStream();
      return myStream;
    }
    @Override
    public boolean isReadOnly() {
      return false;
    }

    @Override
    public DataSourceType getType() {
      return null;
    }

    public InputStream getContentAsStream() {
      return new ByteArrayInputStream(myStream.toByteArray());
    }

    public String getContent(String charsetName) {
      try {
        return myStream.toString(charsetName);
      } catch (UnsupportedEncodingException e) {
        LOG.error(e);
        return null;
      }
    }
  }

  public abstract static class MultiStreamDataSourceBase implements MultiStreamDataSource {
    @NotNull
    @Override
    public InputStream openInputStream(String name) throws IOException {
      throw new UnsupportedOperationException();
    }

    @NotNull
    @Override
    public OutputStream openOutputStream(String name) throws IOException {
      throw new UnsupportedOperationException();
    }

    @Override
    public boolean delete(String name) {
      throw new UnsupportedOperationException();
    }

    @NotNull
    @Override
    public String getLocation() {
      return "in-memory";
    }

    @Override
    public void addListener(@NotNull DataSourceListener listener) {
      throw new UnsupportedOperationException();
    }

    @Override
    public void removeListener(@NotNull DataSourceListener listener) {
      throw new UnsupportedOperationException();
    }

    @Override
    public long getTimestamp() {
      return 0;
    }

    @Override
    public boolean isReadOnly() {
      return true;
    }
  }

  public static class InMemoryMultiStreamDataSource extends MultiStreamDataSourceBase {
    private Map<String, ByteArrayOutputStream> myStreams = new LinkedHashMap<String, ByteArrayOutputStream>();

    @NotNull
    @Override
    public Iterable<String> getAvailableStreams() {
      return myStreams.keySet();
    }
    @NotNull
    @Override
    public OutputStream openOutputStream(String name) throws IOException {
      ByteArrayOutputStream stream = new ByteArrayOutputStream();
      myStreams.put(name, stream);
      return stream;
    }
    @Override
    public boolean isReadOnly() {
      return false;
    }

    @Override
    public DataSourceType getType() {
      return null;
    }

    public String getContent(String name, String charsetName) {
      try {
        ByteArrayOutputStream stream = myStreams.get(name);
        if (stream == null) {
          return null;
        }
        return stream.toString(charsetName);
      } catch (UnsupportedEncodingException e) {
        LOG.error(e);
        return null;
      }
    }
  }
}
