/*
 * Copyright 2003-2015 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.generator.impl.cache;

import jetbrains.mps.extapi.model.SModelBase;
import jetbrains.mps.extapi.model.SModelData;
import jetbrains.mps.generator.TransientModelsProvider.TransientSwapOwner;
import jetbrains.mps.generator.TransientModelsProvider.TransientSwapSpace;
import jetbrains.mps.persistence.binary.BareNodeReader;
import jetbrains.mps.persistence.binary.BareNodeWriter;
import jetbrains.mps.smodel.SModelOperations;
import jetbrains.mps.smodel.TrivialModelDescriptor;
import jetbrains.mps.smodel.persistence.def.ModelReadException;
import jetbrains.mps.util.IterableUtil;
import jetbrains.mps.util.io.ModelInputStream;
import jetbrains.mps.util.io.ModelOutputStream;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SModelReference;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.persistence.PersistenceFacade;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * fyodor, 1/10/11
 */
public abstract class FileSwapOwner implements TransientSwapOwner {

  private static Logger LOG = LogManager.getLogger(FileSwapOwner.class);

  abstract protected File getSwapDir();

  @Override
  public TransientSwapSpace initSwapSpace(String spaceId) {
    return primSwapSpace(spaceId, true);
  }

  @Override
  public TransientSwapSpace accessSwapSpace(String spaceId) {
    return primSwapSpace(spaceId, false);
  }

  private TransientSwapSpace primSwapSpace(String spaceId, boolean init) {
    File swapDir = getSwapDir();
    if (swapDir == null) {
      LOG.error("No swap directory");
      return null;
    }

    File space = new File(swapDir, spaceId);
    if (space.exists()) {
      if (!space.isDirectory()) {
        LOG.error("Swap space is not a directory");
        return null;
      }

      if (init) {
        new FileSwapSpace(space).clear();
      }
    } else {
      if (!init) {
        return null;
      }

      if (!space.mkdirs()) {
        LOG.error("Couldn't create swap space directory");
        return null;
      }
    }

    return new FileSwapSpace(space);
  }

  public static class FileSwapSpace implements TransientSwapSpace {
    private File mySpaceDir;

    public FileSwapSpace(File dir) {
      mySpaceDir = dir;
    }

    @Override
    public boolean swapOut(SModelData model) {
      if (mySpaceDir == null || !mySpaceDir.exists()) throw new IllegalStateException("no swap dir");

      String modelId = model.getReference().getModelId().toString();
      if (modelId == null || modelId.isEmpty()) {
        LOG.error("Bad model id <" + modelId + ">");
        return false;
      }
      modelId = modelId.replaceAll(":", "-");

      File swapFile = new File(mySpaceDir, modelId);
      if (swapFile.exists() && !swapFile.delete()) {
        LOG.error("Couldn't delete swap file");
        return false;
      }

      ArrayList<SNode> roots = new ArrayList<SNode>();
      for (SNode next : model.getRootNodes()) {
        roots.add(next);
      }
      ModelOutputStream mos = null;
      IOException ioex = null;
      try {
        mos = new ModelOutputStream(new FileOutputStream(swapFile));
        saveModel(model.getReference(), roots, mos);
      } catch (IOException e) {
        ioex = e;
        LOG.error(null, e);
      } finally {
        if (mos != null) {
          try {
            mos.close();
          } catch (IOException ignore) {
          }
        }
      }

      return ioex == null;
    }

    @Override
    public <T extends SModelData> T restoreFromSwap(SModelReference mref, T modelData) {
      if (mySpaceDir == null || !mySpaceDir.exists()) throw new IllegalStateException("no swap dir");

      String modelId = mref.getModelId().toString();
      if (modelId == null || modelId.isEmpty()) {
        throw new IllegalStateException("bad modelId");
      }
      modelId = modelId.replaceAll(":", "-");

      File swapFile = new File(mySpaceDir, modelId);
      if (!swapFile.exists()) {
        throw new IllegalStateException("no swap file");
      }

      ModelInputStream mis = null;
      try {
        mis = new ModelInputStream(new FileInputStream(swapFile));
        return loadModel(mref, mis, modelData);
      } catch (IOException e) {
        LOG.error(null, e);
        throw new RuntimeException(e);
      } finally {
        if (mis != null) {
          try {
            mis.close();
          } catch (IOException ignore) {
          }
        }
        if (!swapFile.delete()) {
          LOG.error("Couldn't delete swap file");
        }
      }
    }

    @Override
    public void clear() {
      if (mySpaceDir == null || !mySpaceDir.exists()) throw new IllegalStateException("no swap dir");

      for (File f : mySpaceDir.listFiles()) {
        f.delete();
      }
      mySpaceDir.delete();
      mySpaceDir = null;
    }

    private static final int VERSION = 49;

    public <T extends SModelData> T loadModel(SModelReference modelReference, ModelInputStream is, T model) throws IOException {
      int version = is.readInt();
      if (version != VERSION) {
        return null;
      }

      new BareNodeReader(modelReference, is).readNodesInto(model);
      return model;
    }

    public void saveModel(SModelReference modelReference, List<SNode> roots, ModelOutputStream os) throws IOException {
      os.writeInt(VERSION);
      new BareNodeWriter(modelReference, os).writeNodes(roots);
    }

  }

  // method created for testing
  public static SNode writeAndReadNode(SNode node) throws IOException {
    final SModelReference modelReference = node.getModel().getReference();

    ByteArrayOutputStream os = new ByteArrayOutputStream();
    ModelOutputStream mos = new ModelOutputStream(os);
    BareNodeWriter writer = new BareNodeWriter(modelReference, mos);
    writer.writeNode(node);
    mos.close();

    ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray());
    BareNodeReader reader = new BareNodeReader(modelReference, new ModelInputStream(is));

    return reader.readNode(null);
  }

  // method created for testing
  public static SModel writeAndReadModel(jetbrains.mps.smodel.SModel model) throws IOException, ModelReadException {
    // write
    final ByteArrayOutputStream os = new ByteArrayOutputStream(2048);
    final ModelOutputStream mos = new ModelOutputStream(os);
    mos.writeInt(44);
    new BareNodeWriter(model.getReference(), mos).writeNodes(IterableUtil.asCollection(model.getRootNodes()));
    mos.close();

    final jetbrains.mps.smodel.SModel resultModel = new jetbrains.mps.smodel.SModel(
        PersistenceFacade.getInstance().createModelReference("smodel.long.name.for.testing"));
    ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray());
    ModelInputStream mis = new ModelInputStream(is);

    // read
    final int version = mis.readInt();
    if (version != 44) {
      return null;
    }
    new BareNodeReader(resultModel.getReference(), mis).readNodesInto(resultModel);

    SModelBase result = new TrivialModelDescriptor(resultModel);
    SModelOperations.validateLanguagesAndImports(result, false, false);

    return result;
  }
}
