/**
 * This file is part of the Meeds project (https://meeds.io/).
 *
 * Copyright (C) 2020 - 2025 Meeds Association contact@meeds.io
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */
package org.exoplatform.services.naming;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Stack;
import java.util.concurrent.ConcurrentHashMap;

import javax.naming.NameAlreadyBoundException;
import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.StringRefAddr;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import javax.xml.stream.events.Attribute;
import javax.xml.stream.events.EndElement;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;

/**
 * Class is responsible for binding references at runtime, persists on file and automatically binds after restart.
 * 
 */
public class InitialContextBinder
{

   public static final String BIND_REFERENCES_ELEMENT = "bind-references";

   public static final String REFERENCE_ELEMENT = "reference";

   public static final String PROPERTY_ELEMENT = "property";

   public static final String REFADDR_ELEMENT = "ref-addr";

   public static final String BIND_NAME_ATTR = "bind-name";

   public static final String CLASS_NAME_ATTR = "class-name";

   public static final String FACTORY_ATTR = "factory-name";

   public static final String FACTORY_LOCATION_ATTR = "factory-location";

   /**
    * Initial context binder. 
    */
   protected final InitialContextInitializer initialContextInitializer;

   /**
    * Absolute file path to references's storage.
    */
   protected final String bindingsStorePath;

   /**
    * All current binded references.
    */
   protected Map<String, Reference> bindings;

   /**
    * InitialContextBinder constructor.
    * 
    * @param initialContextInitializer
    *          initial context initializer
    * @param initParams
    *          initialization parameters
    * @throws FileNotFoundException 
    * @throws XMLStreamException
    * @throws NamingException
    */
   InitialContextBinder(InitialContextInitializer initialContextInitializer, String bindingsStorePath)
      throws FileNotFoundException, XMLStreamException, NamingException
   {
      this.initialContextInitializer = initialContextInitializer;
      this.bindings = new ConcurrentHashMap<String, Reference>();
      this.bindingsStorePath = bindingsStorePath;

      if (new File(bindingsStorePath).exists())
      {
         Map<String, Reference> importedRefs = readBindings();
         for (Entry<String, Reference> entry : importedRefs.entrySet())
         {
            bindInternally(entry.getKey(), entry.getValue());
         }
      }
   }

   /**
    * InitialContextBinder constructor.
    */
   InitialContextBinder(InitialContextInitializer initialContextInitializer) throws FileNotFoundException,
      XMLStreamException, NamingException
   {
      this(initialContextInitializer, InitialContextInitializer.DEFAULT_BINDING_STORE_PATH);
   }

   /**
    * Constructs references from params, binds in initial contexts and persists list of all binded
    * references into file.
    * 
    * @param bindName
    *          bind name
    * @param className
    *          class name
    * @param factory
    *          factory name
    * @param factoryLocation
    *          factory location
    * @param refAddr
    *          map of references's properties
    * 
    * @throws NamingException
    *          if error occurs due to binding
    * @throws XMLStreamException 
    * @throws FileNotFoundException
    */
   public void bind(String bindName, String className, String factory, String factoryLocation,
      Map<String, String> refAddr) throws NamingException, FileNotFoundException, XMLStreamException
   {
      Reference reference = new Reference(className, factory, factoryLocation);
      for (Map.Entry<String, String> entry : refAddr.entrySet())
      {
         reference.add(new StringRefAddr(entry.getKey(), entry.getValue()));
      }

      bindInternally(bindName, reference);

      saveBindings();
   }

   /**
    * Binds the reference in initial contexts and persists list of all binded
    * references into file.
    * 
    * @param bindName
    *          bind name
    * @param reference
    *          reference
    * @throws NamingException
    *          if error occurs due to binding
    * @throws FileNotFoundException
    * @throws XMLStreamException
    */
   public void bind(String bindName, Reference reference) throws NamingException, FileNotFoundException,
      XMLStreamException
   {
      bindInternally(bindName, reference);

      saveBindings();
   }

   /**
    * Unbind the Reference with defined name, remove from the bindings list and persists the rest of references
    * into the file.
    * 
    * @param bindName
    *          the Reference's bind name
    * @throws NamingException
    * @throws FileNotFoundException
    * @throws XMLStreamException
    */
   public void unbind(String bindName) throws NamingException, FileNotFoundException,
      XMLStreamException
   {
      initialContextInitializer.getInitialContext().unbind(bindName);
      bindings.remove(bindName);

      saveBindings();
   }

   /**
    * Returns reference associated with defined name.
    * 
    * @param bindName the name on which the reference was binded.
    */
   public Reference getReference(String bindName)
   {
      return bindings.get(bindName);
   }

   private void bindInternally(String bindName, Reference reference) throws NamingException
   {
      try
      {
         initialContextInitializer.getInitialContext().bind(bindName, reference);
      }
      catch (NameAlreadyBoundException e)
      {
         initialContextInitializer.getInitialContext().rebind(bindName, reference);
      }
      bindings.put(bindName, reference);
   }

   /**
    * Export references into xml-file.
    * 
    * @throws XMLStreamException 
    *          if any exception occurs during export
    * @throws FileNotFoundException
    *          if can't open output stream from file
    */
   protected synchronized void saveBindings() throws FileNotFoundException, XMLStreamException
   {
      XMLOutputFactory outputFactory = XMLOutputFactory.newInstance();
      XMLStreamWriter writer =
         outputFactory.createXMLStreamWriter(new FileOutputStream(bindingsStorePath), "UTF-8");

      writer.writeStartDocument("UTF-8", "1.0");
      writer.writeStartElement(BIND_REFERENCES_ELEMENT);

      for (Entry<String, Reference> entry : bindings.entrySet())
      {
         String bindName = entry.getKey();
         Reference reference = entry.getValue();

         writer.writeStartElement(REFERENCE_ELEMENT);
         writer.writeAttribute(BIND_NAME_ATTR, bindName);
         if (reference.getClassName() != null)
         {
            writer.writeAttribute(CLASS_NAME_ATTR, reference.getClassName());
         }
         if (reference.getFactoryClassName() != null)
         {
            writer.writeAttribute(FACTORY_ATTR, reference.getFactoryClassName());
         }
         if (reference.getFactoryClassLocation() != null)
         {
            writer.writeAttribute(FACTORY_LOCATION_ATTR, reference.getFactoryClassLocation());
         }

         writer.writeStartElement(REFADDR_ELEMENT);
         for (int i = 0; i < reference.size(); i++)
         {
            writer.writeStartElement(PROPERTY_ELEMENT);
            writer.writeAttribute(reference.get(i).getType(), (String)reference.get(i).getContent());
            writer.writeEndElement();
         }
         writer.writeEndElement();
         writer.writeEndElement();
      }

      writer.writeEndElement();
      writer.writeEndDocument();
   }

   /**
    * Import references from xml-file. 
    * 
    * @return map with bind name - references
    * 
    * @throws XMLStreamException
    *          if errors occurs during import 
    * @throws FileNotFoundException 
    *          if can't open input stream from file
    */
   protected Map<String, Reference> readBindings() throws FileNotFoundException, XMLStreamException
   {
      Stack<RefEntity> stack = new Stack<RefEntity>();

      Map<String, Reference> importedRefs = new HashMap<String, Reference>();

      XMLInputFactory factory = XMLInputFactory.newInstance();
      XMLEventReader reader =
         factory.createXMLEventReader(new FileInputStream(bindingsStorePath), "UTF-8");

      while (reader.hasNext())
      {
         XMLEvent event = reader.nextEvent();
         switch (event.getEventType())
         {
            case XMLStreamConstants.START_ELEMENT :
               StartElement startElement = event.asStartElement();

               Map<String, String> attr = new HashMap<String, String>();

               Iterator attributes = startElement.getAttributes();
               while (attributes.hasNext())
               {
                  Attribute attribute = (Attribute)attributes.next();
                  attr.put(attribute.getName().getLocalPart(), attribute.getValue());
               }

               String localName = startElement.getName().getLocalPart();
               if (localName.equals(REFERENCE_ELEMENT))
               {
                  String bindName = attr.get(BIND_NAME_ATTR);
                  String className = attr.get(CLASS_NAME_ATTR);
                  String factoryName = attr.get(FACTORY_ATTR);
                  String factoryLocation = attr.get(FACTORY_LOCATION_ATTR);

                  Reference reference = new Reference(className, factoryName, factoryLocation);
                  stack.push(new RefEntity(bindName, reference));
               }
               else if (localName.equals(PROPERTY_ELEMENT))
               {
                  RefEntity refEntity = stack.pop();
                  Reference reference = refEntity.getValue();

                  for (Entry<String, String> entry : attr.entrySet())
                  {
                     reference.add(new StringRefAddr(entry.getKey(), entry.getValue()));
                  }

                  refEntity.setValue(reference);
                  stack.push(refEntity);
               }

               break;
            case XMLStreamConstants.END_ELEMENT :
               EndElement endElement = event.asEndElement();
               localName = endElement.getName().getLocalPart();

               if (localName.equals(REFERENCE_ELEMENT))
               {
                  RefEntity refEntity = stack.pop();
                  importedRefs.put(refEntity.getKey(), refEntity.getValue());
               }
               break;
            default :
               break;
         }
      }

      return importedRefs;
   }

   /**
    * Class implements Map.Entry interface and used to push/pop entity in stack. 
    */
   class RefEntity implements Map.Entry<String, Reference>
   {

      /**
       * Entry key.
       */
      private final String key;

      /**
       * Entry value.
       */
      private Reference value;

      /**
       * RefEntity constructor.
       * 
       * @param key
       *          entry key
       * @param value
       *          entry value       
       */
      public RefEntity(String key, Reference value)
      {
         this.key = key;
         this.value = value;
      }

      /**
       * {@inheritDoc}
       */
      public String getKey()
      {
         return key;
      }

      /**
       * {@inheritDoc}
       */
      public Reference getValue()
      {
         return value;
      }

      /**
       * {@inheritDoc}
       */
      public Reference setValue(Reference value)
      {
         Reference oldValue = this.value;
         this.value = value;

         return oldValue;
      }
   }
}
