/*! ******************************************************************************
 *
 * Pentaho Data Integration
 *
 * Copyright (C) 2002-2022 by Hitachi Vantara : http://www.pentaho.com
 *
 *******************************************************************************
 *
 * 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 org.pentaho.di.shared;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;

import org.apache.commons.vfs2.FileObject;
import org.apache.commons.vfs2.FileSystemException;
import org.apache.commons.io.IOUtils;
import org.pentaho.di.cluster.ClusterSchema;
import org.pentaho.di.cluster.SlaveServer;
import org.pentaho.di.core.Const;
import org.pentaho.di.core.util.Utils;
import org.pentaho.di.core.database.DatabaseMeta;
import org.pentaho.di.core.exception.KettleDatabaseException;
import org.pentaho.di.core.exception.KettleException;
import org.pentaho.di.core.exception.KettleFileException;
import org.pentaho.di.core.exception.KettleXMLException;
import org.pentaho.di.core.logging.LogChannel;
import org.pentaho.di.core.variables.Variables;
import org.pentaho.di.core.vfs.KettleVFS;
import org.pentaho.di.core.xml.XMLHandler;
import org.pentaho.di.i18n.BaseMessages;
import org.pentaho.di.partition.PartitionSchema;
import org.pentaho.di.trans.step.StepMeta;
import org.pentaho.metastore.api.IMetaStore;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.google.common.annotations.VisibleForTesting;

/**
 * Based on a piece of XML, this factory will give back a list of objects. In other words, it does XML de-serialisation
 *
 * @author Matt
 */
public class SharedObjects {
  private static Class<?> PKG = SharedObjects.class; // for i18n purposes, needed by Translator2!!

  private static final String XML_TAG = "sharedobjects";

  private String filename;

  private Map<SharedEntry, SharedObjectInterface> objectsMap;

  private class SharedEntry {
    public String className;
    public String objectName;

    /**
     * @param className
     * @param objectName
     */
    public SharedEntry( String className, String objectName ) {
      this.className = className;
      this.objectName = objectName;
    }

    public boolean equals( Object obj ) {
      SharedEntry sharedEntry = (SharedEntry) obj;
      return className.equals( sharedEntry.className ) && objectName.equalsIgnoreCase( objectName );
    }

    public int hashCode() {
      return className.hashCode() ^ objectName.hashCode();
    }

  }

  public SharedObjects( String sharedObjectsFile ) throws KettleXMLException {
    try {
      this.filename = createFilename( sharedObjectsFile );
      this.objectsMap = new Hashtable<SharedEntry, SharedObjectInterface>();

      LogChannel log = new LogChannel( this );

      // Extra information
      FileObject file = getFileObjectFromKettleVFS( filename );

      // If we have a shared file, load the content, otherwise, just keep this one empty
      if ( file.exists() ) {
        Document document = XMLHandler.loadXMLFile( file );
        Node sharedObjectsNode = XMLHandler.getSubNode( document, XML_TAG );
        if ( sharedObjectsNode != null ) {
          List<SlaveServer> privateSlaveServers = new ArrayList<SlaveServer>();
          List<DatabaseMeta> privateDatabases = new ArrayList<DatabaseMeta>();

          NodeList childNodes = sharedObjectsNode.getChildNodes();
          // First load databases & slaves
          //
          for ( int i = 0; i < childNodes.getLength(); i++ ) {
            Node node = childNodes.item( i );
            String nodeName = node.getNodeName();

            SharedObjectInterface isShared = null;

            if ( nodeName.equals( DatabaseMeta.XML_TAG ) ) {
              try {
                DatabaseMeta sharedDatabaseMeta = new DatabaseMeta( node );
                isShared = sharedDatabaseMeta;
                privateDatabases.add( sharedDatabaseMeta );
              } catch ( KettleXMLException kxe ) {
                // If this is caused because we can't find the database plugin, just log and keep going.
                // The KettleDatabaseException is doubly-wrapped in KettleXMLExceptions, so try to unravel
                Throwable firstCause = kxe.getCause();
                if ( firstCause != null ) {
                  Throwable secondCause = firstCause.getCause();

                  if ( secondCause == null || !( secondCause instanceof KettleDatabaseException ) ) {
                    throw kxe;
                  } else {
                    log.logBasic( kxe.getLocalizedMessage() );
                  }
                } else {
                  throw kxe;
                }
              }
            } else if ( nodeName.equals( SlaveServer.XML_TAG ) ) {
              SlaveServer sharedSlaveServer = new SlaveServer( node );
              isShared = sharedSlaveServer;
              privateSlaveServers.add( sharedSlaveServer );
            }

            if ( isShared != null ) {
              isShared.setShared( true );
              storeObject( isShared );
            }
          }

          // Then load the other objects that might reference databases & slaves
          //
          for ( int i = 0; i < childNodes.getLength(); i++ ) {
            Node node = childNodes.item( i );
            String nodeName = node.getNodeName();

            SharedObjectInterface isShared = null;

            if ( nodeName.equals( StepMeta.XML_TAG ) ) {
              StepMeta stepMeta = new StepMeta( node, privateDatabases, (IMetaStore) null );
              stepMeta.setDraw( false ); // don't draw it, keep it in the tree.
              isShared = stepMeta;
            } else if ( nodeName.equals( PartitionSchema.XML_TAG ) ) {
              isShared = new PartitionSchema( node );
            } else if ( nodeName.equals( ClusterSchema.XML_TAG ) ) {
              isShared = new ClusterSchema( node, privateSlaveServers );
            }

            if ( isShared != null ) {
              isShared.setShared( true );
              storeObject( isShared );
            }
          }
        }
      }
    } catch ( Exception e ) {
      throw new KettleXMLException( BaseMessages.getString( PKG, "SharedOjects.Readingfile.UnexpectedError",
        sharedObjectsFile ), e );
    }
  }

  public static final String createFilename( String sharedObjectsFile ) {
    String filename;
    if ( Utils.isEmpty( sharedObjectsFile ) ) {
      // First fallback is the environment/kettle variable ${KETTLE_SHARED_OBJECTS}
      // This points to the file
      filename = Variables.getADefaultVariableSpace().getVariable( Const.KETTLE_SHARED_OBJECTS );

      // Last line of defence...
      if ( Utils.isEmpty( filename ) ) {
        filename = Const.getSharedObjectsFile();
      }
    } else {
      filename = sharedObjectsFile;
    }
    return filename;
  }

  public SharedObjects() throws KettleXMLException {
    this( null );
  }

  public Map<SharedEntry, SharedObjectInterface> getObjectsMap() {
    return objectsMap;
  }

  public void setObjectsMap( Map<SharedEntry, SharedObjectInterface> objects ) {
    this.objectsMap = objects;
  }

  /**
   * Store the sharedObject in the object map. It is possible to have 2 different types of shared object with the same
   * name. They will be stored separately.
   *
   * @param sharedObject
   */
  public void storeObject( SharedObjectInterface sharedObject ) {
    SharedEntry key = new SharedEntry( sharedObject.getClass().getName(), sharedObject.getName() );
    objectsMap.put( key, sharedObject );
  }

  /**
   * Remove the sharedObject from the object map.
   *
   * @param sharedObject
   */
  public void removeObject( SharedObjectInterface sharedObject ) {
    SharedEntry key = new SharedEntry( sharedObject.getClass().getName(), sharedObject.getName() );
    objectsMap.remove( key );
  }

  public void saveToFile() throws IOException, KettleException {
    FileObject fileObject = getFileObjectFromKettleVFS( filename );
    String backupFileName = createOrGetFileBackup( fileObject );
    writeToFile( fileObject, backupFileName );
  }

  /**
   * Return the shared object with the given class and name
   *
   * @param clazz      The class of the shared object
   * @param objectName the name of the object
   * @return The shared object or null if none was found.
   */
  public SharedObjectInterface getSharedObject( Class<SharedObjectInterface> clazz, String objectName ) {
    return getSharedObject( clazz.getName(), objectName );
  }

  /**
   * Return the shared object with the given class name and object name
   *
   * @param className  The class name of the shared object
   * @param objectName the name of the object
   * @return The shared object or null if none was found.
   */
  public SharedObjectInterface getSharedObject( String className, String objectName ) {
    SharedEntry entry = new SharedEntry( className, objectName );
    return objectsMap.get( entry );
  }

  /**
   * Get the shared database with the specified name
   *
   * @param name The name of the shared database
   * @return The database or null if nothing was found.
   */
  public DatabaseMeta getSharedDatabase( String name ) {
    return (DatabaseMeta) getSharedObject( DatabaseMeta.class.getName(), name );
  }

  /**
   * @return the filename
   */
  public String getFilename() {
    return filename;
  }

  /**
   * @param filename the filename to set
   */
  public void setFilename( String filename ) {
    this.filename = filename;
  }

  /**
   * Write sharedObjects to file. In case of an exception are caught save backup file instead of new one.
   *
   * @param fileObject     is file for writing
   * @param backupFileName is backup file name
   * @throws IOException
   * @throws KettleException
   */
  @VisibleForTesting
  protected void writeToFile( FileObject fileObject, String backupFileName ) throws IOException, KettleException {
    OutputStream outputStream = null;
    PrintStream out = null;
    try {
      outputStream = initOutputStreamUsingKettleVFS( fileObject );

      out = new PrintStream( outputStream );

      out.print( XMLHandler.getXMLHeader( Const.XML_ENCODING ) );
      out.println( "<" + XML_TAG + ">" );

      Collection<SharedObjectInterface> collection = objectsMap.values();
      for ( SharedObjectInterface sharedObject : collection ) {
        String xmlContent = sharedObject.getXML();
        out.println( xmlContent );
      }
      out.println( "</" + XML_TAG + ">" );
    } catch ( Exception e ) {
      // restore file if something wrong
      boolean isRestored = false;
      if ( backupFileName != null ) {
        restoreFileFromBackup( backupFileName );
        isRestored = true;
      }
      throw new KettleException(
        BaseMessages.getString( PKG, "SharedOjects.WriteToFile.ErrorWritingFile", isRestored ), e );
    } finally {
      if ( out != null ) {
        out.flush();
      }
      if ( out != null ) {
        out.close();
      }
      if ( out != null ) {
        outputStream.close();
      }
    }
  }

  private FileObject getFileObjectFromKettleVFS( String filename ) throws KettleFileException {
    return KettleVFS.getFileObject( filename );
  }

  @VisibleForTesting
  protected OutputStream initOutputStreamUsingKettleVFS( FileObject fileObject ) throws IOException {
    return KettleVFS.getOutputStream( fileObject, false );
  }

  /**
   * Call {@link #copyFile(String, String)} method to restore file from backup
   *
   * @param backupFileName
   * @throws IOException
   */
  @VisibleForTesting
  protected void restoreFileFromBackup( String backupFileName ) throws IOException, KettleFileException {
    copyFile( backupFileName, filename );
  }

  /**
   * Call {@link #copyFile(String, String)} method to create file backup
   *
   * @param fileObject
   * @throws IOException
   */
  private String createOrGetFileBackup( FileObject fileObject ) throws IOException, KettleException {
    String backupFileName = filename + ".backup";
    boolean isBackupFileExist = false;
    if ( fileObject.exists() ) {
      isBackupFileExist = createFileBackup( backupFileName );
    } else {
      isBackupFileExist = getBackupFileFromFileSystem( backupFileName );
    }
    return isBackupFileExist ? backupFileName : null;
  }

  private boolean getBackupFileFromFileSystem( String backupFileName ) throws KettleException {
    FileObject fileObject = getFileObjectFromKettleVFS( backupFileName );
    try {
      return fileObject.exists();
    } catch ( FileSystemException e ) {
      return false;
    }
  }

  private boolean createFileBackup( String backupFileName ) throws IOException, KettleFileException {
    return copyFile( filename, backupFileName );
  }

  private boolean copyFile( String src, String dest ) throws KettleFileException, IOException {
    FileObject srcFile = getFileObjectFromKettleVFS( src );
    FileObject destFile = getFileObjectFromKettleVFS( dest );
    try ( InputStream in = KettleVFS.getInputStream( srcFile );
        OutputStream out = KettleVFS.getOutputStream( destFile, false ) ) {
      IOUtils.copy( in, out );
    }
    return true;
  }

  @Override
  public String toString() {
    return getClass().getSimpleName() + ( !Utils.isEmpty( getFilename() ) ? " (" + getFilename() + ")" : "" );
  }
}
