/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.activemq.openwire;

import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;

import org.apache.activemq.command.ActiveMQDestination;
import org.apache.activemq.command.ActiveMQQueue;
import org.apache.activemq.command.ActiveMQTextMessage;
import org.apache.activemq.command.BrokerId;
import org.apache.activemq.command.BrokerInfo;
import org.apache.activemq.command.ConnectionId;
import org.apache.activemq.command.ConsumerId;
import org.apache.activemq.command.DataStructure;
import org.apache.activemq.command.LocalTransactionId;
import org.apache.activemq.command.Message;
import org.apache.activemq.command.MessageAck;
import org.apache.activemq.command.MessageId;
import org.apache.activemq.command.NetworkBridgeFilter;
import org.apache.activemq.command.ProducerId;
import org.apache.activemq.command.SessionId;
import org.apache.activemq.command.TransactionId;
import org.apache.activemq.filter.BooleanExpression;
import org.apache.activemq.openwire.v1.ActiveMQTextMessageTest;
import org.apache.activemq.openwire.v1.BrokerInfoTest;
import org.apache.activemq.openwire.v1.MessageAckTest;
import org.apache.activemq.test.TestSupport;
import org.apache.activemq.util.ByteSequence;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public abstract class DataFileGeneratorTestSupport extends TestSupport {

   protected static final Object[] EMPTY_ARGUMENTS = {};
   private static final Logger LOG = LoggerFactory.getLogger(DataFileGeneratorTestSupport.class);

   private static final Throwable SINGLETON_EXCEPTION = new Exception("shared exception");
   private static final File MODULE_BASE_DIR;
   private static final File CONTROL_DIR;

   static {
      File basedir = null;
      try {
         URL resource = DataFileGeneratorTestSupport.class.getResource("DataFileGeneratorTestSupport.class");
         URI baseURI = new URI(resource.toString()).resolve("../../../../..");
         basedir = new File(baseURI).getCanonicalFile();
      } catch (Exception e) {
         throw new RuntimeException(e);
      }
      MODULE_BASE_DIR = basedir;
      CONTROL_DIR = new File(MODULE_BASE_DIR, "src/test/resources/openwire-control");
   }

   private int counter;
   private OpenWireFormat openWireformat;

   public void xtestControlFileIsValid() throws Exception {
      generateControlFile();
      assertControlFileIsEqual();
   }

   public void testGenerateAndReParsingIsTheSame() throws Exception {
      ByteArrayOutputStream buffer = new ByteArrayOutputStream();
      DataOutputStream ds = new DataOutputStream(buffer);
      Object expected = createObject();
      LOG.info("Created: " + expected);
      openWireformat.marshal(expected, ds);
      ds.close();

      // now lets try parse it back again
      ByteArrayInputStream in = new ByteArrayInputStream(buffer.toByteArray());
      DataInputStream dis = new DataInputStream(in);
      Object actual = openWireformat.unmarshal(dis);
      assertBeansEqual("", new HashSet<>(), expected, actual);

      LOG.info("Parsed: " + actual);
   }

   protected void assertBeansEqual(String message,
                                   Set<Object> comparedObjects,
                                   Object expected,
                                   Object actual) throws Exception {
      assertNotNull("Actual object should be equal to: " + expected + " but was null", actual);
      if (comparedObjects.contains(expected)) {
         return;
      }
      comparedObjects.add(expected);
      Class<? extends Object> type = expected.getClass();
      assertEquals("Should be of same type", type, actual.getClass());
      BeanInfo beanInfo = Introspector.getBeanInfo(type);
      PropertyDescriptor[] descriptors = beanInfo.getPropertyDescriptors();
      for (int i = 0; i < descriptors.length; i++) {
         PropertyDescriptor descriptor = descriptors[i];
         Method method = descriptor.getReadMethod();
         if (method != null) {
            String name = descriptor.getName();
            Object expectedValue = null;
            Object actualValue = null;
            try {
               expectedValue = method.invoke(expected, EMPTY_ARGUMENTS);
               actualValue = method.invoke(actual, EMPTY_ARGUMENTS);
            } catch (Exception e) {
               LOG.info("Failed to access property: " + name);
            }
            assertPropertyValuesEqual(message + name, comparedObjects, expectedValue, actualValue);
         }
      }
   }

   protected void assertPropertyValuesEqual(String name,
                                            Set<Object> comparedObjects,
                                            Object expectedValue,
                                            Object actualValue) throws Exception {
      String message = "Property " + name + " not equal";
      if (expectedValue == null) {
         assertNull("Property " + name + " should be null", actualValue);
      } else if (expectedValue instanceof Object[]) {
         assertArrayEqual(message, comparedObjects, (Object[]) expectedValue, (Object[]) actualValue);
      } else if (expectedValue.getClass().isArray()) {
         assertPrimitiveArrayEqual(message, comparedObjects, expectedValue, actualValue);
      } else {
         if (expectedValue instanceof Exception) {
            assertExceptionsEqual(message, (Exception) expectedValue, actualValue);
         } else if (expectedValue instanceof ByteSequence) {
            assertByteSequencesEqual(message, (ByteSequence) expectedValue, actualValue);
         } else if (expectedValue instanceof DataStructure) {
            assertBeansEqual(message + name, comparedObjects, expectedValue, actualValue);
         } else if (expectedValue instanceof Enumeration) {
            assertEnumerationEqual(message + name, comparedObjects, (Enumeration<?>) expectedValue, (Enumeration<?>) actualValue);
         } else {
            assertEquals(message, expectedValue, actualValue);
         }

      }
   }

   protected void assertArrayEqual(String message,
                                   Set<Object> comparedObjects,
                                   Object[] expected,
                                   Object[] actual) throws Exception {
      assertEquals(message + ". Array length", expected.length, actual.length);
      for (int i = 0; i < expected.length; i++) {
         assertPropertyValuesEqual(message + ". element: " + i, comparedObjects, expected[i], actual[i]);
      }
   }

   protected void assertEnumerationEqual(String message,
                                         Set<Object> comparedObjects,
                                         Enumeration<?> expected,
                                         Enumeration<?> actual) throws Exception {
      while (expected.hasMoreElements()) {
         Object expectedElem = expected.nextElement();
         Object actualElem = actual.nextElement();
         assertPropertyValuesEqual(message + ". element: " + expectedElem, comparedObjects, expectedElem, actualElem);
      }
   }

   protected void assertPrimitiveArrayEqual(String message,
                                            Set<Object> comparedObjects,
                                            Object expected,
                                            Object actual) throws ArrayIndexOutOfBoundsException, IllegalArgumentException, Exception {
      int length = Array.getLength(expected);
      assertEquals(message + ". Array length", length, Array.getLength(actual));
      for (int i = 0; i < length; i++) {
         assertPropertyValuesEqual(message + ". element: " + i, comparedObjects, Array.get(expected, i), Array.get(actual, i));
      }
   }

   protected void assertByteSequencesEqual(String message, ByteSequence expected, Object actualValue) {
      assertTrue(message + ". Actual value should be a ByteSequence but was: " + actualValue, actualValue instanceof ByteSequence);
      ByteSequence actual = (ByteSequence) actualValue;
      int length = expected.getLength();
      assertEquals(message + ". Length", length, actual.getLength());
      int offset = expected.getOffset();
      assertEquals(message + ". Offset", offset, actual.getOffset());
      byte[] data = expected.getData();
      byte[] actualData = actual.getData();
      for (int i = 0; i < length; i++) {
         assertEquals(message + ". Offset " + i, data[offset + i], actualData[offset + i]);
      }
   }

   protected void assertExceptionsEqual(String message, Exception expected, Object actualValue) {
      assertTrue(message + ". Actual value should be an exception but was: " + actualValue, actualValue instanceof Exception);
      Exception actual = (Exception) actualValue;
      assertEquals(message, expected.getMessage(), actual.getMessage());
   }

   @Override
   protected void setUp() throws Exception {
      super.setUp();
      openWireformat = createOpenWireFormat();
   }

   public void generateControlFile() throws Exception {
      CONTROL_DIR.mkdirs();
      File dataFile = new File(CONTROL_DIR, getClass().getName() + ".bin");

      FileOutputStream os = new FileOutputStream(dataFile);
      DataOutputStream ds = new DataOutputStream(os);
      openWireformat.marshal(createObject(), ds);
      ds.close();
   }

   public InputStream generateInputStream() throws Exception {

      ByteArrayOutputStream os = new ByteArrayOutputStream();
      DataOutputStream ds = new DataOutputStream(os);
      openWireformat.marshal(createObject(), ds);
      ds.close();

      return new ByteArrayInputStream(os.toByteArray());
   }

   public void assertControlFileIsEqual() throws Exception {
      File dataFile = new File(CONTROL_DIR, getClass().getName() + ".bin");
      FileInputStream is1 = new FileInputStream(dataFile);
      int pos = 0;
      try {
         InputStream is2 = generateInputStream();
         int a = is1.read();
         int b = is2.read();
         pos++;
         assertEquals("Data does not match control file: " + dataFile + " at byte position " + pos, a, b);
         while (a >= 0 && b >= 0) {
            a = is1.read();
            b = is2.read();
            pos++;
            assertEquals("Data does not match control file: " + dataFile + " at byte position " + pos, a, b);
         }
         is2.close();
      } finally {
         is1.close();
      }
   }

   protected abstract Object createObject() throws Exception;

   protected void populateObject(Object info) throws Exception {
      // empty method to allow derived classes to call super
      // to simplify generated code
   }

   protected OpenWireFormat createOpenWireFormat() {
      OpenWireFormat wf = new OpenWireFormat();
      wf.setCacheEnabled(true);
      wf.setStackTraceEnabled(false);
      wf.setVersion(OpenWireFormat.DEFAULT_WIRE_VERSION);
      return wf;
   }

   protected BrokerId createBrokerId(String text) {
      return new BrokerId(text);
   }

   protected TransactionId createTransactionId(String string) {
      return new LocalTransactionId(createConnectionId(string), ++counter);
   }

   protected ConnectionId createConnectionId(String string) {
      return new ConnectionId(string);
   }

   protected SessionId createSessionId(String string) {
      return new SessionId(createConnectionId(string), ++counter);
   }

   protected ProducerId createProducerId(String string) {
      return new ProducerId(createSessionId(string), ++counter);
   }

   protected ConsumerId createConsumerId(String string) {
      return new ConsumerId(createSessionId(string), ++counter);
   }

   protected MessageId createMessageId(String string) {
      return new MessageId(createProducerId(string), ++counter);
   }

   protected ActiveMQDestination createActiveMQDestination(String string) {
      return new ActiveMQQueue(string);
   }

   protected Message createMessage(String string) throws Exception {
      ActiveMQTextMessage message = (ActiveMQTextMessage) ActiveMQTextMessageTest.SINGLETON.createObject();
      message.setText(string);
      return message;
   }

   protected BrokerInfo createBrokerInfo(String string) throws Exception {
      return (BrokerInfo) BrokerInfoTest.SINGLETON.createObject();
   }

   protected MessageAck createMessageAck(String string) throws Exception {
      return (MessageAck) MessageAckTest.SINGLETON.createObject();
   }

   protected DataStructure createDataStructure(String string) throws Exception {
      return createBrokerInfo(string);
   }

   protected Throwable createThrowable(String string) {
      // we have issues with stack frames not being equal so share the same
      // exception each time
      return SINGLETON_EXCEPTION;
   }

   protected BooleanExpression createBooleanExpression(String string) {
      return new NetworkBridgeFilter(null, new BrokerId(string), 10, 10);
   }

}
