content: /*
  Copyright (c) 2023 Andrew Auclair
  
  Permission is hereby granted, free of charge, to any person obtaining a copy
  of this software and associated documentation files (the "Software"), to deal
  in the Software without restriction, including without limitation the rights
  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  copies of the Software, and to permit persons to whom the Software is
  furnished to do so, subject to the following conditions:
  
  The above copyright notice and this permission notice shall be included in all
  copies or substantial portions of the Software.
  
  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  SOFTWARE.
   */
  package io.github.andrewauclair.moderndocking.internal;
  
  import io.github.andrewauclair.moderndocking.Dockable;
  import io.github.andrewauclair.moderndocking.DockingProperty;
  import io.github.andrewauclair.moderndocking.Property;
  import io.github.andrewauclair.moderndocking.api.LayoutPersistenceAPI;
  import java.io.ByteArrayInputStream;
  import java.io.IOException;
  import java.io.ObjectInputStream;
  import java.io.Serializable;
  import java.lang.reflect.Field;
  import java.lang.reflect.InvocationTargetException;
  import java.util.Arrays;
  import java.util.Base64;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.Objects;
  import java.util.logging.Level;
  import java.util.logging.Logger;
  import java.util.stream.Collectors;
  
  /**
   * Internal utilities for managing dockable properties
   */
  public class DockableProperties {
      private static boolean loadingLegacyFile = false;
  
      private static final Logger logger = Logger.getLogger(DockableProperties.class.getPackageName());
  
      /**
       * Unused. All methods are static
       */
      private DockableProperties() {
      }
  
      /**
       * If true, we're loading a file from before 0.12.0
       *
       * @param legacy Legacy flag
       */
      public static void setLoadingLegacyFile(boolean legacy) {
          loadingLegacyFile = legacy;
      }
  
      /**
       * Set the values of properties on a dockable
       *
       * @param wrapper The dockable to update
       * @param properties The properties to set on the dockable DockingProperty annotated fields
       */
      public static void configureProperties(DockableWrapper wrapper, Map<String, Property> properties) {
          Dockable dockable = wrapper.getDockable();
  
          // remove any existing properties
          for (String key : wrapper.getProperties().keySet()) {
              wrapper.removeProperty(key);
          }
  
          // add all properties to the wrapper
          for (String key : properties.keySet()) {
              wrapper.setProperty(key, properties.get(key));
          }
  
          List<Field> dockingPropFields = Arrays.stream(dockable.getClass().getDeclaredFields())
                  .filter(field -> field.getAnnotation(DockingProperty.class) != null)
                  .collect(Collectors.toList());
  
          for (Field field : dockingPropFields) {
              try {
                  // make sure we can access the field if it is private/protected. only try this if we're sure we can't already access it
                  // because it may result in an IllegalAccessException for trying
                  if (!field.canAccess(dockable)) {
                      field.setAccessible(true);
                  }
  
                  // grab the property and store the value by its name
                  DockingProperty property = field.getAnnotation(DockingProperty.class);
  
                  if (properties.containsKey(property.name())) {
                      Property prop = properties.get(property.name());
  
                      try {
                          if (loadingLegacyFile) {
                              Property.StringProperty legacyProp = (Property.StringProperty) properties.get(property.name());
                              prop = parseProperty(prop.getName(), field.getType().getSimpleName().toString(), legacyProp.getValue());
                          }
                          DockableProperties.validateProperty(field, prop);
                      }
                      catch (Exception e) {
                          // TODO possibly make a new DockingPropertyException
                          throw new RuntimeException(String.format("Dockable: '%s' (%s), default value: '%s' for field '%s' (%s) is invalid", dockable.getPersistentID(), dockable.getClass().getSimpleName(), property.defaultValue(), field.getName(), field.getType().getSimpleName()), e);
                      }
  
                      setProperty(dockable, field, prop);
  
                      // remove the property from the wrapper as it is more specific than the static props
                      wrapper.removeProperty(property.name());
                  }
                  else {
                      // set the default of the type
                      setProperty(dockable, field, createProperty(field, property));
                  }
              } catch (IllegalAccessException | SecurityException e) {
                  // TODO handle this better
                  e.printStackTrace();
              }
          }
  
          dockable.updateProperties();
      }
  
      /**
       * Get the properties from the dockable as a map
       *
       * @param wrapper The dockable to get properties for
       *
       * @return Map of properties
       */
      public static Map<String, Property> saveProperties(DockableWrapper wrapper) {
          Dockable dockable = wrapper.getDockable();
  
          Map<String, Property> properties = new HashMap<>(wrapper.getProperties());
  
          List<Field> dockingPropFields = Arrays.stream(dockable.getClass().getDeclaredFields())
                  .filter(field -> field.getAnnotation(DockingProperty.class) != null)
                  .collect(Collectors.toList());
  
          for (Field field : dockingPropFields) {
              try {
                  // make sure we can access the field if it is private/protected
                  field.setAccessible(true);
  
                  // grab the property and store the value by its name
                  DockingProperty property = field.getAnnotation(DockingProperty.class);
  
                  properties.put(property.name(), getProperty(wrapper, property.name(), field));
              }
              catch (IllegalAccessException ignore) {
              }
          }
  
          return properties;
      }
  
      // throws if validation failed
  
      /**
       * Validate a property instance
       *
       * @param field The field for the property
       * @param property The docking property annotation
       */
      public static void validateProperty(Field field, DockingProperty property) {
          createProperty(field, property);
      }
  
      /**
       * Validate a property
       *
       * @param field The field for the property
       * @param property The internal property instance
       */
      public static void validateProperty(Field field, Property property) {
          Objects.requireNonNull(field);
          Objects.requireNonNull(property);
          Property prop = createProperty(field, property.getName(), property.toString());
          if (prop.getType() != property.getType()) {
              throw new RuntimeException("Type of property does not match type of value.");
          }
      }
  
      /**
       * Parse a property string into the proper type
       *
       * @param property Property name
       * @param type Type of property
       * @param value The string value of the property
       *
       * @return Instance of Property with the proper Java class type
       */
      public static Property parseProperty(String property, String type, String value) {
          if (type.equals("byte")) {
              return new Property.ByteProperty(property, value.isEmpty() ? (byte) 0 : Byte.parseByte(value));
          }
          else if (type.equals("short")) {
              return new Property.ShortProperty(property, value.isEmpty() ? (short) 0 : Short.parseShort(value));
          }
          else if (type.equals("int")) {
              return new Property.IntProperty(property, value.isEmpty() ? 0 : Integer.parseInt(value));
          }
          else if (type.equals("long")) {
              return new Property.LongProperty(property, value.isEmpty() ? (long) 0 : Long.parseLong(value));
          }
          else if (type.equals("float")) {
              return new Property.FloatProperty(property, value.isEmpty() ? 0.0f : Float.parseFloat(value));
          }
          else if (type.equals("double")) {
              return new Property.DoubleProperty(property, value.isEmpty() ? 0.0 : Double.parseDouble(value));
          }
          else if (type.equals("char")) {
              return new Property.CharacterProperty(property, value.isEmpty() ? '\0' : value.charAt(0));
          }
          else if (type.equals("boolean")) {
              return new Property.BooleanProperty(property, !value.isEmpty() && Boolean.parseBoolean(value));
          }
          else if (type.equals("String")) {
              return new Property.StringProperty(property, value);
          }
          else if (type.equals("Serializable")) {
              byte[] data = Base64.getDecoder().decode(value);
              try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(data))) {
                  return new Property.SerializableProperty(property, (Serializable) in.readObject());
              } catch (IOException | ClassNotFoundException e) {
                  throw new RuntimeException(e);
              }
          }
          else {
              throw new RuntimeException("Unsupported property type");
          }
      }
  
      private static Property createProperty(Field field, DockingProperty property) {
          String value = "";
  
          if (!Objects.equals(property.defaultValue(), "__no_default_value__")) {
              value = property.defaultValue();
          }
          return createProperty(field, property.name(), value);
      }
  
      private static Property createProperty(Field field, String property, String value) {
          Class<?> type = field.getType();
  
          if (type == byte.class) {
              return new Property.ByteProperty(property, value.isEmpty() ? (byte) 0 : Byte.parseByte(value));
          }
          else if (type == short.class) {
              return new Property.ShortProperty(property, value.isEmpty() ? (short) 0 : Short.parseShort(value));
          }
          else if (type == int.class) {
              return new Property.IntProperty(property, value.isEmpty() ? 0 : Integer.parseInt(value));
          }
          else if (type == long.class) {
              return new Property.LongProperty(property, value.isEmpty() ? (long) 0 : Long.parseLong(value));
          }
          else if (type == float.class) {
              return new Property.FloatProperty(property, value.isEmpty() ? 0.0f : Float.parseFloat(value));
          }
          else if (type == double.class) {
              return new Property.DoubleProperty(property, value.isEmpty() ? 0.0 : Double.parseDouble(value));
          }
          else if (type == char.class) {
              return new Property.CharacterProperty(property, value.isEmpty() ? '\0' : value.charAt(0));
          }
          else if (type == boolean.class) {
              return new Property.BooleanProperty(property, !value.isEmpty() && Boolean.parseBoolean(value));
          }
          else if (type == String.class) {
              return new Property.StringProperty(property, value);
          }
          else if (Serializable.class.isInstance(type)) {
              if (value.isEmpty()) {
                  try {
                      return new Property.SerializableProperty(property, (Serializable) type.getConstructor().newInstance());
                  } catch (NoSuchMethodException | InstantiationException | IllegalAccessException |
                           InvocationTargetException ex) {
                      throw new RuntimeException(ex);
                  }
              }
              byte[] data = Base64.getDecoder().decode(value);
              try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(data))) {
                  return new Property.SerializableProperty(property, (Serializable) in.readObject());
              } catch (IOException | ClassNotFoundException e) {
                  throw new RuntimeException(e);
              }
          }
  //        else if (type.isEnum()) {
  //            return Integer.toString(((Enum<?>) field.get(dockable)).ordinal());
  //            return "";
  //        }
          else {
              throw new RuntimeException("Unsupported property type");
          }
      }
  
      private static Property getProperty(DockableWrapper wrapper, String property, Field field) throws IllegalAccessException {
          Dockable dockable = wrapper.getDockable();
  
          Class<?> type = field.getType();
  
          if (type == byte.class) {
              return new Property.ByteProperty(property, (byte) field.get(dockable));
          }
          else if (type == short.class) {
              return new Property.ShortProperty(property, (short) field.get(dockable));
          }
          else if (type == int.class) {
              return new Property.IntProperty(property, (int) field.get(dockable));
          }
          else if (type == long.class) {
              return new Property.LongProperty(property, (long) field.get(dockable));
          }
          else if (type == float.class) {
              return new Property.FloatProperty(property, (float) field.get(dockable));
          }
          else if (type == double.class) {
              return new Property.DoubleProperty(property, (double) field.get(dockable));
          }
          else if (type == char.class) {
              return new Property.CharacterProperty(property, (char) field.get(dockable));
          }
          else if (type == boolean.class) {
              return new Property.BooleanProperty(property, (boolean) field.get(dockable));
          }
          else if (type == String.class) {
              return new Property.StringProperty(property, (String) field.get(dockable));
          }
          else if (Serializable.class.isInstance(type)) {
              return new Property.SerializableProperty(property, (Serializable) field.get(dockable));
          }
  //        else if (type.isEnum()) {
  //            return Integer.toString(((Enum<?>) field.get(dockable)).ordinal());
  //            return "";
  //        }
          else {
              throw new RuntimeException("Unsupported property type");
          }
      }
  
      private static void setProperty(Dockable dockable, Field field, Property value) throws IllegalAccessException {
          Class<?> type = field.getType();
  
          if (type == byte.class) {
              field.set(dockable, ((Property.ByteProperty) value).getValue());
          }
          else if (type == short.class) {
              field.set(dockable, ((Property.ShortProperty) value).getValue());
          }
          else if (type == int.class) {
              field.set(dockable, ((Property.IntProperty) value).getValue());
          }
          else if (type == long.class) {
              field.set(dockable, ((Property.LongProperty) value).getValue());
          }
          else if (type == float.class) {
              field.set(dockable, ((Property.FloatProperty) value).getValue());
          }
          else if (type == double.class) {
              field.set(dockable, ((Property.DoubleProperty) value).getValue());
          }
          else if (type == char.class) {
              field.set(dockable, ((Property.CharacterProperty) value).getValue());
          }
          else if (type == boolean.class) {
              field.set(dockable, ((Property.BooleanProperty) value).getValue());
          }
          else if (type == String.class) {
              field.set(dockable, ((Property.StringProperty) value).getValue());
          }
          else if (Serializable.class.isInstance(type)) {
              Serializable serializable = ((Property.SerializableProperty) value).getValue();
              field.set(dockable, type.cast(serializable));
  
          }
  //        else if (type.isEnum()) {
  //            int ordinal = Integer.parseInt(value);
  //
  //            field.set(dockable, type.getEnumConstants()[ordinal]);
  //        }
          else {
              throw new RuntimeException("Unsupported property type");
          }
      }
  }
  