/*
 * Copyright 2013 Martin Kouba
 *
 * 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.trimou.engine.config;

import org.trimou.util.Strings;

/**
 *
 * @author Martin Kouba
 */
final class ConfigurationProperties {

    private ConfigurationProperties() {
    }

    /**
     * Build the property key for the given name and prefix parts. The name is
     * converted to CamelCase (underscore is used as a delimiter):
     *
     *

     * MY_PROPERTY_NAME
     *

     *
     * becomes
     *
     *

     * myPropertyName
     *

     *
     * @param propertyName
     * @param prefixParts
     * @return the key
     */
    static String buildPropertyKey(String propertyName, String[] prefixParts) {
        return buildPropertyKey(propertyName, Strings.UNDERSCORE, prefixParts);
    }

    /**
     * Build the property key for the given name and prefix parts. The name is
     * converted to CamelCase (using the specified delimiter; delimiters are
     * removed in the end).
     *
     * @param propertyName
     * @param delimiter
     * @param prefixParts
     * @return the key
     */
    static String buildPropertyKey(String propertyName, String delimiter,
            String[] prefixParts) {
        StringBuilder key = new StringBuilder();
        for (final String prefixPart : prefixParts) {
            key.append(prefixPart);
            key.append(Strings.DOT);
        }
        key.append(
                Strings.uncapitalize(
                        Strings.replace(
                                Strings.capitalizeFully(propertyName,
                                        delimiter.toCharArray()[0]),
                                delimiter, "")));
        return key.toString();
    }

    /**
     *
     * @param defaultValueType
     * @param value
     * @return the converted value
     */
    static Object convertConfigValue(Class<?> defaultValueType, Object value) {
        if (defaultValueType.equals(String.class)) {
            return value.toString();
        } else if (defaultValueType.equals(Boolean.class)) {
            return Boolean.valueOf(value.toString());
        } else if (defaultValueType.equals(Long.class)) {
            return Long.valueOf(value.toString());
        } else if (defaultValueType.equals(Integer.class)) {
            return Integer.valueOf(value.toString());
        }
        throw new IllegalStateException(
                "Unsupported default value type: " + defaultValueType);
    }

}


/*
 * Copyright 2013 Xi CHEN
 *
 * 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 com.seanchenxi.gwt.storage.client;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.GWT.UncaughtExceptionHandler;
import com.google.gwt.core.client.JavaScriptException;
import com.google.gwt.storage.client.Storage;
import com.google.gwt.user.client.rpc.SerializationException;
import com.google.web.bindery.event.shared.HandlerRegistration;
import com.seanchenxi.gwt.storage.client.cache.StorageCache;
import com.seanchenxi.gwt.storage.shared.StorageUtils;

import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;

/**
 * Extends the GWT HTML5 Storage API, by adding Object Value Support.
 *
 *
 *

 * You can obtain a Storage by either invoking
 * {@link #getLocalStorage()} or
 * {@link #getSessionStorage()}.
 *

 *
 *
 *

 * If Web Storage is NOT supported in the browser, these methods return
 * null.
 *

 *
 *
 *

 * Note: Storage events into other windows are not supported.
 *

 *
 *
 *

 * This may not be supported on all browsers.
 *

 *
 *
 * @see W3C Web Storage - Storage
 * @see Can I use... - Web Storage
 * @see GWT Developer's Guide - Client-side Storage (Web Storage)
 * @see
 *      com.google.gwt.storage.client.Storage
 */
public final class StorageExt {

  private static StorageExt localStorage;
  private static StorageExt sessionStorage;

  /**
   * Returns a Local Storage.
   *
   * @return the localStorage instance, or null if Web Storage is NOT supported.
   */
  public static StorageExt getLocalStorage() {
    if (localStorage == null && Storage.isLocalStorageSupported()) {
      localStorage = new StorageExt(Storage.getLocalStorageIfSupported());
    }
    return localStorage;
  }

  /**
   * Returns a Session Storage.
   *
   * @return the sessionStorage instance, or null if Web Storage is NOT supported.
   */
  public static StorageExt getSessionStorage() {
    if (sessionStorage == null && Storage.isSessionStorageSupported()) {
      sessionStorage = new StorageExt(Storage.getSessionStorageIfSupported());
    }
    return sessionStorage;
  }

  private StorageChangeEvent.Level eventLevel;
  private Set<StorageChangeEvent.Handler> handlers;
  private final StorageCache cache;
  private final Storage storage;

  /**
   * This class can never be instantiated externally. Use
   * {@link #getLocalStorage()} ()} or
   * {@link #getSessionStorage()} ()} instead.
   */
  private StorageExt(Storage storage) {
    assert storage != null : "Storage can not be null, check your browser's HTML 5 support state.";
    this.storage = storage;
    this.cache = GWT.create(StorageCache.class);
    this.eventLevel = StorageChangeEvent.Level.STRING;
  }

  /**
   * Registers an event handler for {@link StorageChangeEvent}
   *
   * @param handler an event handler instance
   * @return {@link HandlerRegistration} used to remove this handler
   */
  public HandlerRegistration addStorageChangeHandler(final StorageChangeEvent.Handler handler) {
    if(handler == null)
      throw new IllegalArgumentException("Handler can not be null");
    ensureHandlerSet().add(handler);
    return new HandlerRegistration() {
      @Override
      public void removeHandler() {
        if (handlers != null) {
          handlers.remove(handler);
          if (handlers.isEmpty()) {
            handlers = null;
          }
        }
      }
    };
  }

  /**
   * Removes all items in the Storage, and its cache if activated
   *
   * @see W3C Web
   *      Storage - Storage.clear()
   */
  public void clear() {
    storage.clear();
    cache.clear();
    fireEvent(StorageChangeEvent.ChangeType.CLEAR, null, null, null, null, null);
  }

  /**
   * Clear all cached serialized object from configured {@link StorageCache}
   */
  public void clearCache() {
    cache.clear();
  }

  /**
   * Test if this storage contains a value for the specified key.
   *
   *

   * {@link StorageKeyFactory} is preferred to get a {@link StorageKey} instance for primitive types.
   *

   *
   * @param key the key whose presence in this storage is to be tested
   * @param <T> the type of stored value
   * @return <tt>true</tt> if this storage contains a value for the specified key.
   */
  public <T> boolean containsKey(StorageKey<T> key) {
    return storage.getItem(key.name()) != null;
  }

  /**
   * Returns the value in the Storage associated with the specified key,
   * or {@code null} if this Storage contains no mapping for the key.
   *

   * Note : Deserialization will be performed to return a correct value type.

   * {@link StorageKeyFactory} is preferred to get a {@link StorageKey} instance for primitive types.
   *

   *
   * @param key the key to a value in the Storage
   * @param <T> the type of stored value
   * @return the value associated with the given key
   * @throws SerializationException
   * @see W3C Web
   *      Storage - Storage.getItem(k)
   */
  public <T> T get(StorageKey<T> key) throws SerializationException {
    T item = cache.get(key);
    if (item == null) {
      item = StorageUtils.deserialize(key.getClazz(), storage.getItem(key.name()));
      cache.put(key, item);
    }
    return item;
  }

  /**
   * Get directly the stored string value associated with the specified key.
   *
   *

   * Note : No deserialization will be performed
   *

   *
   * @param key the key to a value in the Storage
   * @return the stored string value associated with the given key
   * @see W3C Web
   *      Storage - Storage.getItem(k)
   */
  public String getString(String key) {
    return storage.getItem(key);
  }

  /**
   * Returns the key at the specified index.
   *
   * @param index the index of the key
   * @return the key at the specified index in this Storage
   * @see W3C Web
   *      Storage - Storage.key(n)
   */
  public String key(int index) {
    return storage.key(index);
  }

  /**
   * Store the specified value with the specified key in this storage.
   *
   *

   * Note: null value is not allowed.

   * If the storage previously contained a mapping for the key, the old
   * value is replaced.

   * {@link StorageKeyFactory} is preferred to get a {@link StorageKey} instance for primitive types.
   *

   *
   * @param key key with which the specified value is to be associated
   * @param value value to be associated with the specified key
   * @throws SerializationException
   * @throws StorageQuotaExceededException
   */
  public <T> void put(StorageKey<T> key, T value) throws SerializationException, StorageQuotaExceededException {
    if(value == null){
      throw new NullPointerException();
    }
    try {
      String data = StorageUtils.serialize(key.getClazz(), value);
      // Update store and cache
      String oldData = storage.getItem(key.name());
      storage.setItem(key.name(), data);
      T oldValue = cache.put(key, value);
      fireEvent(StorageChangeEvent.ChangeType.PUT, key, value, oldValue, data, oldData);
    } catch (JavaScriptException e) {
      String msg = e.getMessage();
      if (msg != null && msg.contains("QUOTA") && msg.contains("DOM")) {
        throw new StorageQuotaExceededException(key, e);
      }
      throw e;
    }
  }

  /**
   * Removes the record for the specified key from this storage if present.
   *
   *

   * {@link StorageKeyFactory} is preferred to get a {@link StorageKey} instance for primitive types
   *

   *
   * @param key key whose mapping is to be removed from the map
   * @param <T> the type of stored value
   */
  public <T extends Serializable> void remove(StorageKey<T> key) {
    String data = storage.getItem(key.name());
    storage.removeItem(key.name());
    T value = cache.remove(key);
    fireEvent(StorageChangeEvent.ChangeType.REMOVE, key, null, value, null, data);
  }

  /**
   * Set Event Level.
   *
   *

   * Note : set to {@link StorageChangeEvent.Level#STRING},
   * will prevent the deserialization process in event creation,

   * This matters only if the old value (object value) is wanted in every storage change event.
   *

   *
   * @param eventLevel the event detail level
   * @see StorageChangeEvent
   */
  public void setEventLevel(StorageChangeEvent.Level eventLevel) {
    this.eventLevel = eventLevel;
  }

  /**
   * Returns the number of items in this Storage.
   *
   * @return number of items in this Storage
   * @see W3C Web
   *      Storage - Storage.length()
   */
  public int size() {
    return storage.getLength();
  }

  /**
   * Ensure {@link StorageChangeEvent.Handler} registration set instance
   */
  private Set<StorageChangeEvent.Handler> ensureHandlerSet() {
    if (handlers == null) {
      handlers = new HashSet<>();
    }
    return handlers;
  }

  /**
   * Fire {@link StorageChangeEvent}
   */
  private <T> void fireEvent(StorageChangeEvent.ChangeType changeType, StorageKey<T> key, T value, T oldVal, String data, String oldData) {
    UncaughtExceptionHandler ueh = GWT.getUncaughtExceptionHandler();
    if (handlers != null && !handlers.isEmpty()) {
      T oldValue = oldVal;
      if (oldValue == null && oldData != null && StorageChangeEvent.Level.OBJECT.equals(eventLevel)) {
        try {
          oldValue = StorageUtils.deserialize(key.getClazz(), data);
        } catch (SerializationException e) {
          if (ueh != null)
            ueh.onUncaughtException(e);
          oldValue = null;
        }
      }

      final StorageChangeEvent event = new StorageChangeEvent(changeType, key, value, oldValue, data, oldData);
      for (StorageChangeEvent.Handler handler : handlers) {
        try{
          handler.onStorageChange(event);
        }catch(Exception e){
          if(ueh != null){
            ueh.onUncaughtException(e);
          }
        }
      }
    }
  }

}


/*
 * Copyright 2013 Xi CHEN
 *
 * 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 com.seanchenxi.gwt.storage.client.serializer;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.ListIterator;

import com.google.gwt.core.client.JsonUtils;
import com.google.gwt.user.client.rpc.SerializationException;
import com.google.gwt.user.client.rpc.impl.AbstractSerializationStreamWriter;
import com.google.gwt.user.client.rpc.impl.Serializer;

final class StorageSerializationStreamWriter extends AbstractSerializationStreamWriter {

  private static final String APOSTROPHE = "'";
  private static final String BLANK = "";
  private static final String CLOSE_BRACKET = "]";
  private static final String COMMA = ",";
  private static final String OPEN_BRACKET = "[";

  private static void append(StringBuffer sb, String token) {
    assert (token != null);
    sb.append(token).append(COMMA);
  }

  private final Serializer serializer;
  private ArrayList<String> tokenList;

  public StorageSerializationStreamWriter(Serializer serializer) {
    this.serializer = serializer;
  }

  public <T> String getSerializationSignature(Class<? super T> clazz) {
    return serializer.getSerializationSignature(clazz);
  }

  @Override
  public void prepareToWrite() {
    super.prepareToWrite();
    tokenList = new ArrayList<>();
  }

  @Override
  public String toString() {
    StringBuffer buffer = new StringBuffer(OPEN_BRACKET);
    writePayload(buffer);
    writeStringTable(buffer);
    writeHeader(buffer);
    return JsonUtils.escapeJsonForEval(buffer.append(CLOSE_BRACKET).toString());
  }

  @Override
  public void writeLong(long value) {
    append(APOSTROPHE + longToBase64(value) + APOSTROPHE);
  }

  @Override
  protected void append(String token) {
    assert (token != null);
    tokenList.add(tokenList.size(), token);
  }

  @Override
  protected String getObjectTypeSignature(Object o) {
    Class<?> clazz = o.getClass();
    if (o instanceof Enum<?>) {
      Enum<?> e = (Enum<?>) o;
      clazz = e.getDeclaringClass();
    }
    return serializer.getSerializationSignature(clazz);
  }

  @Override
  protected void serialize(Object instance, String typeSignature) throws SerializationException {
    serializer.serialize(this, instance, typeSignature);
  }

  private String escapeString(String toEscape) {
    if (toEscape == null) return null;
    return JsonUtils.escapeValue(toEscape);
  }

  private void writeHeader(StringBuffer buffer) {
     append(buffer, String.valueOf(getFlags()));
     buffer.append(getVersion());
  }

  private void writePayload(StringBuffer buffer) {
    ListIterator<String> list = tokenList.listIterator(tokenList.size());
    while (list.hasPrevious()) {
      append(buffer, list.previous());
    }
  }

  private void writeStringTable(StringBuffer buffer) {
    StringBuffer sb = new StringBuffer(OPEN_BRACKET);
    Iterator<String> stringTable = getStringTable().iterator();
    while (stringTable.hasNext()) {
      append(sb, escapeString(stringTable.next()));
    }
    sb.append(CLOSE_BRACKET);
    int index = sb.lastIndexOf(COMMA);
    String table = (index > -1) ? sb.replace(index, index + 1, BLANK).toString() : sb.toString();
    append(buffer, table);
  }

  /**
   * Copy from {@link com.google.gwt.user.client.rpc.impl.AbstractSerializationStream#longToBase64(long)}
   *
   * Return an optionally single-quoted string containing a base-64 encoded
   * version of the given long value.
   *
   * Keep this synchronized with the version in Base64Utils.
   */
  static String longToBase64(long value) {
    // Convert to ints early to avoid need for long ops
    int low = (int) (value & 0xffffffff);
    int high = (int) (value >> 32);

    StringBuilder sb = new StringBuilder();
    boolean haveNonZero = base64Append(sb, (high >> 28) & 0xf, false);
    haveNonZero = base64Append(sb, (high >> 22) & 0x3f, haveNonZero);
    haveNonZero = base64Append(sb, (high >> 16) & 0x3f, haveNonZero);
    haveNonZero = base64Append(sb, (high >> 10) & 0x3f, haveNonZero);
    haveNonZero = base64Append(sb, (high >> 4) & 0x3f, haveNonZero);
    int v = ((high & 0xf) << 2) | ((low >> 30) & 0x3);
    haveNonZero = base64Append(sb, v, haveNonZero);
    haveNonZero = base64Append(sb, (low >> 24) & 0x3f, haveNonZero);
    haveNonZero = base64Append(sb, (low >> 18) & 0x3f, haveNonZero);
    haveNonZero = base64Append(sb, (low >> 12) & 0x3f, haveNonZero);
    base64Append(sb, (low >> 6) & 0x3f, haveNonZero);
    base64Append(sb, low & 0x3f, true);

    return sb.toString();
  }

  private static boolean base64Append(StringBuilder sb, int digit, boolean haveNonZero) {
    if (digit > 0) {
      haveNonZero = true;
    }
    if (haveNonZero) {
      int c;
      if (digit < 26) {
        c = 'A' + digit;
      } else if (digit < 52) {
        c = 'a' + digit - 26;
      } else if (digit < 62) {
        c = '0' + digit - 52;
      } else if (digit == 62) {
        c = '$';
      } else {
        c = '_';
      }
      sb.append((char) c);
    }
    return haveNonZero;
  }
}


/*
 * Copyright 2013 Xi CHEN
 *
 * 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 com.seanchenxi.gwt.storage.client;

import com.google.gwt.user.client.rpc.IsSerializable;

import java.io.Serializable;

/**
 * Storage Key Factory
 *
 *

 *
 *     Choose type correspond method for all primitive type values' key,

 *     Use {@link #serializableKey(String)} for Serializable object type
 *     Use {@link #isSerializableKey(String)} for IsSerializable object type
 *
 *

 *
 */
public class StorageKeyFactory {

  /**
   * Returns non-primitive type value's storage key.
   *
   * @param keyName name of storage key
   * @param <T> type of value
   * @return non-primitive type value's storage key
   * @deprecated use {@link #isSerializableKey(String)} or {@link #serializableKey(String)} instead
   */
  @Deprecated
  public static <T> StorageKey<T> objectKey(String keyName){
    return new StorageKey<>(keyName, Object.class);
  }

  /**
   * Returns IsSerializable type value's storage key.
   *
   * @param keyName name of storage key
   * @param <T> type of value
   * @return IsSerializable type value's storage key
   */
  public static <T extends IsSerializable> StorageKey<T> isSerializableKey(String keyName){
      return new StorageKey<>(keyName, IsSerializable.class);
  }

  /**
   * Returns Serializable type value's storage key.
   *
   * @param keyName name of storage key
   * @param <T> type of value
   * @return Serializable type value's storage key
   */
  public static <T extends Serializable> StorageKey<T> serializableKey(String keyName){
      return new StorageKey<>(keyName, Serializable.class);
  }

  public static StorageKey<Boolean> boolKey(String keyName){
    return new StorageKey<>(keyName, Boolean.class);
  }

  public static StorageKey<Byte> byteKey(String keyName){
    return new StorageKey<>(keyName, Byte.class);
  }

  public static StorageKey<Character> charKey(String keyName){
    return new StorageKey<Character>(keyName, Character.class);
  }

  public static StorageKey<Double> doubleKey(String keyName){
    return new StorageKey<>(keyName, Double.class);
  }

  public static StorageKey<Float> floatKey(String keyName){
    return new StorageKey<>(keyName, Float.class);
  }

  public static StorageKey<Integer> intKey(String keyName){
    return new StorageKey<>(keyName, Integer.class);
  }

  public static StorageKey<Long> longKey(String keyName){
    return new StorageKey<>(keyName, Long.class);
  }

  public static StorageKey<Short> shortKey(String keyName){
    return new StorageKey<>(keyName, Short.class);
  }

  public static StorageKey<String> stringKey(String keyName){
    return new StorageKey<>(keyName, String.class);
  }

  public static StorageKey<Boolean[]> boxedBoolArrayKey(String keyName){
    return new StorageKey<>(keyName, Boolean[].class);
  }

  public static StorageKey<Byte[]> boxedByteArrayKey(String keyName){
    return new StorageKey<>(keyName, Byte[].class);
  }

  public static StorageKey<Character[]> boxedCharArrayKey(String keyName){
    return new StorageKey<>(keyName, Character[].class);
  }

  public static StorageKey<Double[]> boxedDoubleArrayKey(String keyName){
    return new StorageKey<>(keyName, Double[].class);
  }

  public static StorageKey<Float[]> boxedFloatArrayKey(String keyName){
    return new StorageKey<>(keyName, Float[].class);
  }

  public static StorageKey<Integer[]> boxedIntArrayKey(String keyName){
    return new StorageKey<>(keyName, Integer[].class);
  }

  public static StorageKey<Long[]> boxedLongArrayKey(String keyName){
    return new StorageKey<>(keyName, Long[].class);
  }

  public static StorageKey<Short[]> boxedShortArrayKey(String keyName){
    return new StorageKey<>(keyName, Short[].class);
  }

  public static StorageKey<String[]> stringArrayKey(String keyName){
    return new StorageKey<>(keyName, String[].class);
  }

  public static StorageKey<boolean[]> boolArrayKey(String keyName){
    return new StorageKey<>(keyName, boolean[].class);
  }

  public static StorageKey<byte[]> byteArrayKey(String keyName){
    return new StorageKey<>(keyName, byte[].class);
  }

  public static StorageKey<char[]> charArrayKey(String keyName){
    return new StorageKey<>(keyName, char[].class);
  }

  public static StorageKey<double[]> doubleArrayKey(String keyName){
    return new StorageKey<>(keyName, double[].class);
  }

  public static StorageKey<float[]> floatArrayKey(String keyName){
    return new StorageKey<>(keyName, float[].class);
  }

  public static StorageKey<int[]> intArrayKey(String keyName){
    return new StorageKey<>(keyName, int[].class);
  }

  public static StorageKey<long[]> longArrayKey(String keyName){
    return new StorageKey<>(keyName, long[].class);
  }

  public static StorageKey<short[]> shortArrayKey(String keyName){
    return new StorageKey<>(keyName, short[].class);
  }

}


/*
 * Copyright 2013 Xi CHEN
 *
 * 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 com.seanchenxi.gwt.storage.rebind;

import com.google.gwt.core.ext.BadPropertyValueException;
import com.google.gwt.core.ext.ConfigurationProperty;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.PropertyOracle;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JRealClassType;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.TypeOracle;

import com.seanchenxi.gwt.storage.client.StorageKeyProvider;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;

/**
 * Created by: Xi
 */
public abstract class StorageTypeFinder {

  public static final String SERIALIZATION_CONFIG = "storage-serialization.xml";

  public static final String PROP_RPC_BLACKLIST = "rpc.blacklist";
  public static final String PROP_STORAGE_BLACKLIST = "storage.blacklist";
  public static final String PROP_TYPE_FINDER = "storage.type.finder";
  public static final List<String> TYPE_FINDER_VALUES = Arrays.asList("rpc", "xml", "mix", "none");

  public static StorageTypeFinder getInstance(GeneratorContext context, TreeLogger logger) throws UnableToCompleteException{
    final List<StorageTypeFinder> typeFinders = getStorageTypeFinders(context, logger);
    switch (typeFinders.size()){
      case 0:
        return new TypeRpcFinder(context, logger);
      case 1:
        return typeFinders.get(0);
      default:
        return new TypeMixFinder(context, logger, typeFinders);
    }
  }

  private static List<StorageTypeFinder> getStorageTypeFinders(GeneratorContext context, TreeLogger logger) throws UnableToCompleteException {
    final List<StorageTypeFinder> typeFinders = new ArrayList<>();

    JClassType keyProviderIntf = context.getTypeOracle().findType(StorageKeyProvider.class.getName());
    if(keyProviderIntf.getSubtypes() != null && keyProviderIntf.getSubtypes().length > 1){
      typeFinders.add(new TypeProviderFinder(context, logger));
    }

    PropertyOracle propertyOracle = context.getPropertyOracle();
    try {
      ConfigurationProperty property = propertyOracle.getConfigurationProperty(PROP_TYPE_FINDER);
      String value = property == null ? TYPE_FINDER_VALUES.get(0) : property.getValues().get(0).toLowerCase();
      switch(TYPE_FINDER_VALUES.indexOf(value)){
        case 0:
          typeFinders.add(new TypeRpcFinder(context, logger));
          break;
        case 1:
          typeFinders.add(new TypeXmlFinder(context, logger));
          break;
        case 2:
          typeFinders.add(new TypeRpcFinder(context, logger));
          typeFinders.add(new TypeXmlFinder(context, logger));
          break;
        default:
          break;
      }
    } catch (BadPropertyValueException e) {
      logger.branch(TreeLogger.DEBUG, "Could not find property " + PROP_TYPE_FINDER, e);
    }
    return typeFinders;
  }

  protected final TreeLogger logger;
  protected final TypeOracle typeOracle;

  private StorageTypeFilter typeFilter;

  public StorageTypeFinder(GeneratorContext context, TreeLogger logger) {
    this.logger = logger;
    this.typeOracle = context.getTypeOracle();
  }

  public void setTypeFilter(StorageTypeFilter typeFilter) {
    this.typeFilter = typeFilter;
  }

  protected boolean addIfIsValidType(final Set<JType> serializables, JType jType) {
    if(jType == null) return false;
    // we don't filter interface types here, GWT's SerializableTypeOracle will treat them
    if(!isAllowed(jType.isClass())){
      logger.branch(TreeLogger.DEBUG, jType + " was filtered.");
      return false;
    }else{
      serializables.add(jType);
      logger.branch(TreeLogger.TRACE, "add " + jType + " as storage serializable.");
      return true;
    }
  }

  public boolean isAllowed(JClassType type){
    if(this.typeFilter == null){
      return true;
    }

    String name = getBaseTypeName(type);
    // For types not handled by getBaseTypeName just return true.
    if (name == null) {
      return true;
    }
    return typeFilter.isIncluded(logger, name);
  }

  /**
   * Returns a simple types, including classes and
   * interfaces, parameterized, and raw types. Null is returned for other types
   * such as arrays and type parameters (e.g., 'E' in java.util.List<E>) because
   * filtering is meaningless for such types.
   */
  protected String getBaseTypeName(JClassType type) {
    if(type == null){
      return null;
    }
    if (type instanceof JRealClassType) {
      return type.getQualifiedSourceName();
    } else if (type.isParameterized() != null) {
      return type.isParameterized().getBaseType().getQualifiedSourceName();
    } else if (type.isRawType() != null) {
      return type.isRawType().getQualifiedSourceName();
    }
    return type.getQualifiedSourceName();
  }

  public abstract Set<JType> findStorageTypes() throws UnableToCompleteException;
}


/*
 * Copyright 2015 - 2019 Michael Rapp
 *
 * 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 de.mrapp.android.view;

import android.animation.Animator;
import android.animation.Animator.AnimatorListener;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.graphics.drawable.RippleDrawable;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.StateListDrawable;
import android.graphics.drawable.shapes.OvalShape;
import android.os.Build;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewPropertyAnimator;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Interpolator;
import android.widget.ImageButton;
import android.widget.RelativeLayout;

import androidx.annotation.ColorInt;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StyleRes;
import androidx.core.content.ContextCompat;
import de.mrapp.android.util.ThemeUtil;
import de.mrapp.android.util.ViewUtil;
import de.mrapp.util.Condition;

/**
 * A floating action button, which has been designed according to the Material design guidelines.
 *

 * Refer to http://www.google.com/design/spec/components/buttons-floating-action- button.html for
 * further information on the Material design guidelines.
 *
 * @author Michael Rapp
 * @since 1.0.0
 * @deprecated Since version 2.1.3 this class is deprecated in favor of the
 * FloatingActionButton, which is now part of Android's official Design support
 * library
 */
@Deprecated
public class FloatingActionButton extends RelativeLayout {

    /**
     * Contains all possible sizes of a floating action button.
     */
    public enum Size {

        /**
         * A floating action button's normal size.
         */
        NORMAL(0),

        /**
         * A floating action button's small size.
         */
        SMALL(1),

        /**
         * A floating action button's large size.
         */
        LARGE(2);

        /**
         * The value, which represents the size.
         */
        private final int value;

        /**
         * Creates a new size of a floating action button.
         *
         * @param value
         *         The value, which represents the size, as an {@link Integer} value
         */
        Size(final int value) {
            this.value = value;
        }

        /**
         * Returns the value, which represents the size.
         *
         * @return The value, which represents the size, as an {@link Integer} value
         */
        protected int getValue() {
            return value;
        }

        /**
         * Returns the size, which corresponds to a specific value.
         *
         * @param value
         *         The value, which represents the size, as an {@link Integer} value
         * @return The size, which corresponds to the given value, as a value of the enum {@link
         * Size}
         */
        protected static Size fromValue(final int value) {
            for (Size currentSize : values()) {
                if (currentSize.value == value) {
                    return currentSize;
                }
            }

            throw new IllegalArgumentException("Invalid enum value: " + value);
        }

    }

    /**
     * The image button, which is used to show the floating action button's background and icon.
     */
    private ImageButton imageButton;

    /**
     * The floating action button's size.
     */
    private Size size;

    /**
     * The floating action button's color.
     */
    private int color;

    /**
     * The color, which is used as an overlay, when the floating action button is activated.
     */
    private int activatedColor;

    /**
     * The color, which is used as an overlay, when the floating action button is pressed.
     */
    private int pressedColor;

    /**
     * The color, which is used as an overlay, when the floating action button is disabled.
     */
    private int disabledColor;

    /**
     * The duration of the animation, which may be used to change the visibility of the floating
     * action button, in milliseconds.
     */
    private long visibilityAnimationDuration;

    /**
     * The animator, which may be used to change the visibility of the floating action button.
     */
    private ViewPropertyAnimator visibilityAnimator;

    /**
     * Initializes the view.
     *
     * @param attributeSet
     *         The attribute set, the view's attributes should be obtained from, as an instance of
     *         the type {@link AttributeSet} or null, if no attributes should be obtained
     */
    private void initialize(@Nullable final AttributeSet attributeSet) {
        inflateLayout();
        obtainStyledAttributes(attributeSet);
        adaptShadow();
        adaptImageButtonSize();
        adaptImageButtonBackground();
    }

    /**
     * Inflates the view's layout.
     */
    private void inflateLayout() {
        imageButton = new ImageButton(getContext());
        LayoutParams layoutParams = new LayoutParams(0, 0);
        layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE);
        addView(imageButton, layoutParams);
    }

    /**
     * Obtains the view's attributes from a specific attribute set.
     *
     * @param attributeSet
     *         The attribute set, the view's attributes should be obtained from, as an instance of
     *         the type {@link AttributeSet} or null, if no attributes should be obtained
     */
    private void obtainStyledAttributes(@Nullable final AttributeSet attributeSet) {
        TypedArray typedArray =
                getContext().obtainStyledAttributes(attributeSet, R.styleable.FloatingActionButton);

        try {
            obtainSize(typedArray);
            obtainColor(typedArray);
            obtainActivatedColor(typedArray);
            obtainPressedColor(typedArray);
            obtainDisabledColor(typedArray);
            obtainIcon(typedArray);
            obtainVisibilityAnimationDuration(typedArray);
        } finally {
            typedArray.recycle();
        }
    }

    /**
     * Obtains the floating action button's size from a specific typed array.
     *
     * @param typedArray
     *         The typed array, the color should be obtained from, as an instance of the class
     *         {@link TypedArray}. The typed array may not be null
     */
    private void obtainSize(@NonNull final TypedArray typedArray) {
        Size defaultSize = Size.NORMAL;
        size = Size.fromValue(
                typedArray.getInt(R.styleable.FloatingActionButton_size, defaultSize.getValue()));
    }

    /**
     * Obtains the floating action button's color from a specific typed array.
     *
     * @param typedArray
     *         The typed array, the color should be obtained from, as an instance of the class
     *         {@link TypedArray}. The typed array may not be null
     */
    private void obtainColor(@NonNull final TypedArray typedArray) {
        int defaultColor = ThemeUtil.getColor(getContext(), R.attr.colorAccent);
        color = typedArray.getColor(R.styleable.FloatingActionButton_android_color, defaultColor);
    }

    /**
     * Obtains the floating action button's activated color from a specific typed array.
     *
     * @param typedArray
     *         The typed array, the activated color should be obtained from, as an instance of the
     *         class {@link TypedArray}. The typed array may not be null
     */
    private void obtainActivatedColor(@NonNull final TypedArray typedArray) {
        int defaultActivatedColor = getControlActivatedColor();
        activatedColor = typedArray
                .getColor(R.styleable.FloatingActionButton_activatedColor, defaultActivatedColor);
    }

    /**
     * Obtains the floating action button's pressed color from a specific typed array.
     *
     * @param typedArray
     *         The typed array, the pressed color should be obtained from, as an instance of the
     *         class {@link TypedArray}. The typed array may not be null
     */
    private void obtainPressedColor(@NonNull final TypedArray typedArray) {
        int defaultPressedColor = getControlHighlightColor();
        pressedColor = typedArray
                .getColor(R.styleable.FloatingActionButton_pressedColor, defaultPressedColor);
    }

    /**
     * Obtains the floating action button's disabled color from a specific typed array.
     *
     * @param typedArray
     *         The typed array, the disabled color should be obtained from, as an instance of the
     *         class {@link TypedArray}. The typed array may not be null
     */
    private void obtainDisabledColor(@NonNull final TypedArray typedArray) {
        int defaultDisabledColor =
                ContextCompat.getColor(getContext(), R.color.floating_action_button_disabled_color);
        disabledColor = typedArray
                .getColor(R.styleable.FloatingActionButton_disabledColor, defaultDisabledColor);
    }

    /**
     * Obtains the floating action button's icon from a specific typed array.
     *
     * @param typedArray
     *         The typed array, the ripple color should be obtained from, as an instance of the
     *         class {@link TypedArray}. The typed array may not be null
     */
    private void obtainIcon(@NonNull final TypedArray typedArray) {
        Drawable icon = typedArray.getDrawable(R.styleable.FloatingActionButton_android_icon);
        setIcon(icon);
    }

    /**
     * Obtains the duration of the animation, which may be used to changed the visibility of the
     * floating action button, from a specific typed array.
     *
     * @param typedArray
     *         The typed array, the animation duration should be obtained from, as an instance of
     *         the class {@link TypedArray}. The typed array may not be null
     */
    private void obtainVisibilityAnimationDuration(@NonNull final TypedArray typedArray) {
        int defaultAnimationDuration = getResources()
                .getInteger(R.integer.floating_action_button_visibility_animation_duration);
        int duration = typedArray
                .getInteger(R.styleable.FloatingActionButton_visibilityAnimationDuration,
                        defaultAnimationDuration);
        setVisibilityAnimationDuration(duration);
    }

    /**
     * Adapts the shadow of the floating action button, depending on its size.
     */
    private void adaptShadow() {
        if (getSize() == Size.NORMAL) {
            setBackgroundResource(R.drawable.floating_action_button_shadow_normal);
        } else if (getSize() == Size.SMALL) {
            setBackgroundResource(R.drawable.floating_action_button_shadow_small);
        } else {
            setBackgroundResource(R.drawable.floating_action_button_shadow_large);
        }
    }

    /**
     * Adapts the size of the image button, which is used to show the floating image button's
     * background and icon, depending on the floating button's size.
     */
    private void adaptImageButtonSize() {
        int pixelSize = getPixelSize();
        LayoutParams layoutParams = (LayoutParams) imageButton.getLayoutParams();
        layoutParams.width = pixelSize;
        layoutParams.height = pixelSize;
        imageButton.setLayoutParams(layoutParams);
        imageButton.requestLayout();
    }

    /**
     * Adapts the background of the image button, which is used to show the floating image button's
     * background and icon, depending on the floating button's colors.
     */
    @SuppressLint("NewApi")
    private void adaptImageButtonBackground() {
        Drawable background = createStateListBackgroundDrawable();

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            Drawable rippleDrawable = new RippleDrawable(
                    new ColorStateList(new int[][]{{}}, new int[]{getPressedColor()}), background,
                    null);
            ViewUtil.setBackground(imageButton, rippleDrawable);
        } else {
            ViewUtil.setBackground(imageButton, background);
        }
    }

    /**
     * Creates and returns a state list drawable, which can be used as the floating action button
     * background and adapts the background color depending on the button's current state.
     *
     * @return The drawable, which has been created, as an instance of the class {@link Drawable}
     */
    private Drawable createStateListBackgroundDrawable() {
        StateListDrawable drawable = new StateListDrawable();

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
            drawable.addState(new int[]{android.R.attr.state_enabled, android.R.attr.state_pressed},
                    createPressedBackgroundDrawable());
        }

        drawable.addState(new int[]{android.R.attr.state_enabled, android.R.attr.state_activated},
                createActivatedBackgroundDrawable());
        drawable.addState(new int[]{android.R.attr.state_enabled},
                createBackgroundDrawable(getColor()));
        drawable.addState(new int[]{}, createDisabledBackgroundDrawable());
        return drawable;
    }

    /**
     * Creates and returns a drawable, which can be used as the floating action button's background,
     * when it is activated.
     *
     * @return The drawable, which has been created, as an instance of the class {@link Drawable}
     */
    private Drawable createActivatedBackgroundDrawable() {
        Drawable drawable = createBackgroundDrawable(getColor());
        Drawable hoverDrawable = createBackgroundDrawable(getActivatedColor());
        return new LayerDrawable(new Drawable[]{drawable, hoverDrawable});
    }

    /**
     * Creates and returns a drawable, which can be used as the floating action button's background,
     * when it is pressed.
     *
     * @return The drawable, which has been created, as an instance of the class {@link Drawable}
     */
    private Drawable createPressedBackgroundDrawable() {
        Drawable drawable = createBackgroundDrawable(getColor());
        Drawable hoverDrawable = createBackgroundDrawable(getPressedColor());
        return new LayerDrawable(new Drawable[]{drawable, hoverDrawable});
    }

    /**
     * Creates and returns a drawable, which can be used as the floating action button's background,
     * when it is disabled.
     *
     * @return The drawable, which has been created, as an instance of the class {@link Drawable}
     */
    private Drawable createDisabledBackgroundDrawable() {
        Drawable drawable = createBackgroundDrawable(getColor());
        Drawable hoverDrawable = createBackgroundDrawable(getDisabledColor());
        return new LayerDrawable(new Drawable[]{drawable, hoverDrawable});
    }

    /**
     * Creates and returns a drawable with a specific color, which can be used as the floating
     * action button's background.
     *
     * @param color
     *         The color of the background as an {@link Integer} value
     * @return The drawable, which has been created, as an instance of the class {@link Drawable}
     */
    private Drawable createBackgroundDrawable(@ColorInt final int color) {
        OvalShape shape = new OvalShape();
        ShapeDrawable drawable = new ShapeDrawable(shape);
        drawable.getPaint().setColor(color);
        return drawable;
    }

    /**
     * Returns the size of the floating action button in pixels, depending on its current size.
     *
     * @return The size of the floating action button in pixels as an {@link Integer} value
     */
    private int getPixelSize() {
        if (getSize() == Size.NORMAL) {
            return getResources().getDimensionPixelSize(R.dimen.floating_action_button_size_normal);
        } else if (getSize() == Size.SMALL) {
            return getResources().getDimensionPixelSize(R.dimen.floating_action_button_size_small);
        } else {
            return getResources().getDimensionPixelSize(R.dimen.floating_action_button_size_large);
        }
    }

    /**
     * Returns the color of the theme attribute R.attr.colorControlHighlight.
     *
     * @return The color of the theme attribute R.attr.colorControlHighlight as an
     * {@link Integer} value
     */
    private int getControlHighlightColor() {
        TypedArray typedArray = getContext().getTheme()
                .obtainStyledAttributes(new int[]{R.attr.colorControlHighlight});
        return typedArray.getColor(0, 0);
    }

    /**
     * Returns the color of the theme attribute R.attr.colorControlActivated.
     *
     * @return The color of the theme attribute R.attr.colorControlActivated as an
     * {@link Integer} value
     */
    private int getControlActivatedColor() {
        TypedArray typedArray = getContext().getTheme()
                .obtainStyledAttributes(new int[]{R.attr.colorControlActivated});
        return typedArray.getColor(0, 0);
    }

    /**
     * Animates changing the visibility of the floating action button.
     *
     * @param visibility
     *         The visibility, which should be set, as an {@link Integer} value. The visibility may
     *         be View.VISIBLE, View.INVISIBLE or View.GONE
     * @param duration
     *         The duration of the animation in milliseconds as a {@link Long} value
     */
    private void animateVisibility(final int visibility, final long duration) {
        if (visibilityAnimator != null) {
            visibilityAnimator.cancel();
        }

        AnimatorListener listener = createVisibilityAnimatorListener(visibility);
        float targetScale = visibility == View.VISIBLE ? 1 : 0;
        long animationDuration = Math.round(Math.abs(getScaleX() - targetScale) * duration);
        Interpolator interpolator = new AccelerateDecelerateInterpolator();
        visibilityAnimator =
                animate().setInterpolator(interpolator).scaleX(targetScale).scaleY(targetScale)
                        .setDuration(animationDuration).setListener(listener);
    }

    /**
     * Creates and returns a listener, which allows to adapt the visibility of the floating action
     * button, depending on the progress of an animation, which is used to change the visibility.
     *
     * @param visibility
     *         The visibility, which is set by the observed animation, as an {@link Integer} value.
     *         The visibility may be View.VISIBLE, View.INVISIBLE or
     *         View.GONE
     * @return The listener, which has been created, as an instance of the type {@link
     * AnimatorListener}
     */
    private AnimatorListener createVisibilityAnimatorListener(final int visibility) {
        return new AnimatorListener() {

            @Override
            public void onAnimationStart(final Animator animation) {
                if (visibility == View.VISIBLE) {
                    FloatingActionButton.super.setVisibility(visibility);
                }
            }

            @Override
            public void onAnimationRepeat(final Animator animation) {

            }

            @Override
            public void onAnimationEnd(final Animator animation) {
                FloatingActionButton.super.setVisibility(visibility);
                visibilityAnimator = null;
            }

            @Override
            public void onAnimationCancel(final Animator animation) {
                visibilityAnimator = null;
            }

        };
    }

    /**
     * Creates a new floating action button, which has been designed according to the Material
     * design guidelines.
     *
     * @param context
     *         The context, which should be used by the view, as an instance of the class {@link
     *         Context}. The context may not be null
     */
    public FloatingActionButton(@NonNull final Context context) {
        this(context, null);
    }

    /**
     * Creates a new floating action button, which has been designed according to the Material
     * design guidelines.
     *
     * @param context
     *         The context, which should be used by the view, as an instance of the class {@link
     *         Context}. The context may not be null
     * @param attributeSet
     *         The attribute set, the view's attributes should be obtained from, as an instance of
     *         the type {@link AttributeSet} or null, if no attributes should be obtained
     */
    public FloatingActionButton(@NonNull final Context context,
                                @Nullable final AttributeSet attributeSet) {
        super(context, attributeSet);
        initialize(attributeSet);
    }

    /**
     * Creates a new floating action button, which has been designed according to the Material
     * design guidelines.
     *
     * @param context
     *         The context, which should be used by the view, as an instance of the class {@link
     *         Context}. The context may not be null
     * @param attributeSet
     *         The attribute set, the view's attributes should be obtained from, as an instance of
     *         the type {@link AttributeSet} or null, if no attributes should be obtained
     * @param defaultStyle
     *         The default style to apply to this view. If 0, no style will be applied (beyond what
     *         is included in the theme). This may either be an attribute resource, whose value will
     *         be retrieved from the current theme, or an explicit style resource
     */
    public FloatingActionButton(@NonNull final Context context,
                                @Nullable final AttributeSet attributeSet,
                                @StyleRes final int defaultStyle) {
        super(context, attributeSet, defaultStyle);
        initialize(attributeSet);
    }

    /**
     * Creates a new floating action button, which has been designed according to the Material
     * design guidelines.
     *
     * @param context
     *         The context, which should be used by the view, as an instance of the class {@link
     *         Context}. The context may not be null
     * @param attributeSet
     *         The attribute set, the view's attributes should be obtained from, as an instance of
     *         the type {@link AttributeSet} or null, if no attributes should be obtained
     * @param defaultStyle
     *         The default style to apply to this view. If 0, no style will be applied (beyond what
     *         is included in the theme). This may either be an attribute resource, whose value will
     *         be retrieved from the current theme, or an explicit style resource
     * @param defaultStyleResource
     *         A resource identifier of a style resource that supplies default values for the view,
     *         used only if the default style is 0 or can not be found in the theme. Can be 0 to not
     *         look for defaults
     */
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public FloatingActionButton(@NonNull final Context context,
                                @Nullable final AttributeSet attributeSet,
                                @StyleRes final int defaultStyle, final int defaultStyleResource) {
        super(context, attributeSet, defaultStyle, defaultStyleResource);
        initialize(attributeSet);
    }

    /**
     * Returns the floating action button's size.
     *
     * @return The floating action button's size as a value of the enum {@link Size}. The size may
     * either be NORMAL or SMALL
     */
    public final Size getSize() {
        return size;
    }

    /**
     * Sets the floating action button's size.
     *
     * @param size
     *         The size, which should be set, as a value of the enum {@link Size}. The size must
     *         either be NORMAL or SMALL
     */
    public final void setSize(@NonNull final Size size) {
        Condition.INSTANCE.ensureNotNull(size, "The size may not be null");
        this.size = size;
        adaptShadow();
        adaptImageButtonSize();
        requestLayout();
    }

    /**
     * Returns the floating action button's icon.
     *
     * @return The floating action button's icon as an instance of the class {@link Drawable} or
     * null, if no icon has been set
     */
    public final Drawable getIcon() {
        return imageButton.getDrawable();
    }

    /**
     * Sets the floating action button's icon.
     *
     * @param icon
     *         The icon, which should be set, as an instance of the class {@link Drawable} or null,
     *         if no icon should be set
     */
    public final void setIcon(@Nullable final Drawable icon) {
        imageButton.setImageDrawable(icon);
    }

    /**
     * Sets the floating action button's icon.
     *
     * @param resourceId
     *         The resource id of the icon, which should be set, as an {@link Integer} value. The
     *         resource id must correspond to a valid drawable resource
     */
    public final void setIcon(@DrawableRes final int resourceId) {
        imageButton.setImageResource(resourceId);
    }

    /**
     * Returns the floating action button's color.
     *
     * @return The floating action button's color as an {@link Integer} value
     */
    public final int getColor() {
        return color;
    }

    /**
     * Sets the floating action button's color.
     *
     * @param color
     *         The color, which should be set, as an {@link Integer} value
     */
    public final void setColor(@ColorInt final int color) {
        this.color = color;
        adaptImageButtonBackground();
    }

    /**
     * Returns the color, which is used as an overlay, when the floating action button is
     * activated.
     *
     * @return The color, which is used as an overlay, when the floating action button is activated,
     * as an {@link Integer} value
     */
    public final int getActivatedColor() {
        return activatedColor;
    }

    /**
     * Sets the color, which should be used as an overlay, when the floating action button is
     * activated.
     *
     * @param color
     *         The color, which should be set, as an {@link Integer} value
     */
    public final void setActivatedColor(@ColorInt final int color) {
        this.activatedColor = color;
        adaptImageButtonBackground();
    }

    /**
     * Returns the color, which is used as an overlay, when the floating action button is pressed.
     *
     * @return The color, which is used as an overlay, when the floating action button is pressed,
     * as an {@link Integer} value
     */
    public final int getPressedColor() {
        return pressedColor;
    }

    /**
     * Sets the color, which should be used as an overlay, when the floating action button is
     * pressed.
     *
     * @param color
     *         The color, which should be set, as an {@link Integer} value
     */
    public final void setPressedColor(@ColorInt final int color) {
        this.pressedColor = color;
        adaptImageButtonBackground();
    }

    /**
     * Returns the color, which is used as an overlay, when the floating action button is disabled.
     *
     * @return The color, which is used as an overlay, when the floating action button is disabled,
     * as an {@link Integer} value
     */
    public final int getDisabledColor() {
        return disabledColor;
    }

    /**
     * Sets the color, which should be used as an overlay, when the floating action button is
     * disabled.
     *
     * @param color
     *         The color, which should be set, as an {@link Integer} value
     */
    public final void setDisabledColor(@ColorInt final int color) {
        this.disabledColor = color;
        adaptImageButtonBackground();
    }

    /**
     * Returns the duration of the animation, which may be used to change the visibility of the
     * floating action button.
     *
     * @return The duration of the animation in milliseconds as a {@link Long} value
     */
    public final long getVisibilityAnimationDuration() {
        return visibilityAnimationDuration;
    }

    /**
     * Sets the duration of the animation, which may be used to change the visibility of the
     * floating action button.
     *
     * @param duration
     *         The duration, which should be set, in milliseconds as a {@link Long} value
     */
    public final void setVisibilityAnimationDuration(final long duration) {
        Condition.INSTANCE.ensureAtLeast(duration, 0, "The animation duration must be at least 0");
        this.visibilityAnimationDuration = duration;
    }

    /**
     * Sets the visibility of the floating action button.
     *
     * @param visibility
     *         The visibility, which should be set, as an {@link Integer} value. The visibility may
     *         be View.VISIBLE, View.INVISIBLE or View.GONE
     * @param animate
     *         True, if changing the visibility should be animated, false otherwise
     */
    public final void setVisibility(final int visibility, final boolean animate) {
        if (animate) {
            animateVisibility(visibility, getVisibilityAnimationDuration());
        } else {
            setVisibility(visibility);
        }
    }

    @Override
    public final void setVisibility(final int visibility) {
        super.setVisibility(visibility);
        float scale = visibility == View.VISIBLE ? 1 : 0;
        setScaleX(scale);
        setScaleY(scale);
    }

    @Override
    public final void setOnClickListener(final OnClickListener listener) {
        imageButton.setOnClickListener(listener);
    }

    @Override
    public final void setOnLongClickListener(final OnLongClickListener listener) {
        imageButton.setOnLongClickListener(listener);
    }

    @Override
    public final void setEnabled(final boolean enabled) {
        super.setEnabled(enabled);
        imageButton.setEnabled(enabled);
    }

    @Override
    protected final void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int shadowSize =
                getResources().getDimensionPixelSize(R.dimen.floating_action_button_shadow_size);
        int pixelSize = getPixelSize() + shadowSize;
        setMeasuredDimension(pixelSize, pixelSize);
    }

}


/*
 * Copyright 2015 - 2019 Michael Rapp
 *
 * 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 de.mrapp.android.view;

import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.os.Build;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StyleRes;
import android.util.AttributeSet;
import android.view.View;

import de.mrapp.android.util.ThemeUtil;
import de.mrapp.android.view.drawable.CircularProgressDrawable;

/**
 * A circular progress bar, which has been designed according to the Material design guidelines.
 *
 * Refer to http://www.google.com/design/spec/components/progress-activity.html#
 * progress-activity-types-of-indicators for further information on the Material design guidelines.
 *
 * @author Michael Rapp
 * @since 1.0.0
 */
public class CircularProgressBar extends View {

    /**
     * The color of the circular progress bar.
     */
    private int color;

    /**
     * The thickness of the circular progress bar.
     */
    private int thickness;

    /**
     * The drawable, which is shown by the view.
     */
    private CircularProgressDrawable circularProgressDrawable;

    /**
     * Initializes the view.
     *
     * @param attributeSet
     *         The attribute set, the view's attributes should be obtained from, as an instance of
     *         the type {@link AttributeSet} or null, if no attributes should be obtained
     */
    private void initialize(@Nullable final AttributeSet attributeSet) {
        obtainStyledAttributes(attributeSet);
        initializeDrawable();
    }

    /**
     * Obtains the view's attributes from a specific attribute set.
     *
     * @param attributeSet
     *         The attribute set, the view's attributes should be obtained from, as an instance of
     *         the type {@link AttributeSet} or null, if no attributes should be obtained
     */
    private void obtainStyledAttributes(@Nullable final AttributeSet attributeSet) {
        TypedArray typedArray =
                getContext().obtainStyledAttributes(attributeSet, R.styleable.CircularProgressBar);

        try {
            obtainColor(typedArray);
            obtainThickness(typedArray);
        } finally {
            typedArray.recycle();
        }
    }

    /**
     * Obtains the circular progress bar's color from a specific typed array.
     *
     * @param typedArray
     *         The typed array, the color should be obtained from, as an instance of the class
     *         {@link TypedArray}. The typed array may not be null
     */
    private void obtainColor(@NonNull final TypedArray typedArray) {
        int defaultColor = ThemeUtil.getColor(getContext(), R.attr.colorAccent);
        color = typedArray.getColor(R.styleable.CircularProgressBar_android_color, defaultColor);
    }

    /**
     * Obtains the progress bar's thickness from a specific typed array.
     *
     * @param typedArray
     *         The typed array, the thickness should be obtained from, as an instance of the class
     *         {@link TypedArray}. The typed array may not be null
     */
    private void obtainThickness(@NonNull final TypedArray typedArray) {
        int defaultThickness = getContext().getResources()
                .getDimensionPixelSize(R.dimen.circular_progress_bar_thickness_normal);
        thickness = typedArray
                .getDimensionPixelSize(R.styleable.CircularProgressBar_android_thickness,
                        defaultThickness);
    }

    /**
     * Initializes the drawable, which is shown by the view.
     */
    private void initializeDrawable() {
        circularProgressDrawable = new CircularProgressDrawable(getColor(), getThickness());
        circularProgressDrawable.setCallback(this);
    }

    /**
     * Creates a new circular progress bar, which has been designed according to the Material design
     * guidelines.
     *
     * @param context
     *         The context, the view should belong to, as an instance of the class {@link Context}.
     *         The context may not be null
     */
    public CircularProgressBar(@NonNull final Context context) {
        this(context, null);
    }

    /**
     * Creates a new circular progress bar, which has been designed according to the Material design
     * guidelines.
     *
     * @param context
     *         The context, the view should belong to, as an instance of the class {@link Context}.
     *         The context may not be null
     * @param attributeSet
     *         The attribute set, the view's attributes should be obtained from, as an instance of
     *         the type {@link AttributeSet} or null, if no attributes should be obtained
     */
    public CircularProgressBar(@NonNull final Context context,
                               @Nullable final AttributeSet attributeSet) {
        super(context, attributeSet);
        initialize(attributeSet);
    }

    /**
     * Creates a new circular progress bar, which has been designed according to the Material design
     * guidelines.
     *
     * @param context
     *         The context, the view should belong to, as an instance of the class {@link Context}.
     *         The context may not be null
     * @param attributeSet
     *         The attribute set, the view's attributes should be obtained from, as an instance of
     *         the type {@link AttributeSet} or null, if no attributes should be obtained
     * @param defaultStyle
     *         The default style to apply to this view. If 0, no style will be applied (beyond what
     *         is included in the theme). This may either be an attribute resource, whose value will
     *         be retrieved from the current theme, or an explicit style resource
     */
    public CircularProgressBar(final Context context, @Nullable final AttributeSet attributeSet,
                               @StyleRes final int defaultStyle) {
        super(context, attributeSet, defaultStyle);
        initialize(attributeSet);
    }

    /**
     * Creates a new circular progress bar, which has been designed according to the Material design
     * guidelines.
     *
     * @param context
     *         The context, which should be used by the view, as an instance of the class {@link
     *         Context}. The context may not be null
     * @param attributeSet
     *         The attribute set, the view's attributes should be obtained from, as an instance of
     *         the type {@link AttributeSet} or null, if no attributes should be obtained
     * @param defaultStyle
     *         The default style to apply to this  view. If 0, no style will be applied (beyond what
     *         is included in the theme). This may either be an attribute resource, whose value will
     *         be retrieved from the current theme, or an explicit style resource
     * @param defaultStyleResource
     *         A resource identifier of a style resource that supplies default values for the view,
     *         used only if the default style is 0 or can not be found in the theme. Can be 0 to not
     *         look for defaults
     */
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public CircularProgressBar(@NonNull final Context context,
                               @Nullable final AttributeSet attributeSet,
                               @StyleRes final int defaultStyle, final int defaultStyleResource) {
        super(context, attributeSet, defaultStyle, defaultStyleResource);
        initialize(attributeSet);
    }

    /**
     * Returns the color of the circular progress bar.
     *
     * @return The color of the circular progress bar as an {@link Integer} value
     */
    public final int getColor() {
        return color;
    }

    /**
     * Sets the color of the circular progress bar.
     *
     * @param color
     *         The color, which should be set, as an {@link Integer} value
     */
    public final void setColor(@ColorInt final int color) {
        this.color = color;
        initializeDrawable();
        invalidate();
    }

    /**
     * Returns the thickness of the circular progress bar.
     *
     * @return The thickness of the circular progress bar in pixels as an {@link Integer} value
     */
    public final int getThickness() {
        return thickness;
    }

    /**
     * Sets the thickness of the circular progress bar.
     *
     * @param thickness
     *         The thickness, which should be set, in pixels as an {@link Integer} value
     */
    public final void setThickness(final int thickness) {
        this.thickness = thickness;
        initializeDrawable();
        invalidate();
    }

    @Override
    public final void draw(final Canvas canvas) {
        super.draw(canvas);
        circularProgressDrawable.draw(canvas);
    }

    @Override
    protected final void onAttachedToWindow() {
        super.onAttachedToWindow();

        if (circularProgressDrawable != null) {
            circularProgressDrawable.start();
        }
    }

    @Override
    protected final void onVisibilityChanged(@NonNull final View changedView,
                                             final int visibility) {
        super.onVisibilityChanged(changedView, visibility);

        if (circularProgressDrawable != null) {
            if (visibility == VISIBLE) {
                circularProgressDrawable.start();
            } else {
                circularProgressDrawable.stop();
            }
        }
    }

    @Override
    protected final void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
        int measuredWidth =
                MeasureSpec.getSize(widthMeasureSpec) + getPaddingLeft() + getPaddingRight();
        int measuredHeight =
                MeasureSpec.getSize(heightMeasureSpec) + getPaddingTop() + getPaddingBottom();
        setMeasuredDimension(measuredWidth, measuredHeight);
    }

    @Override
    protected final void onSizeChanged(final int width, final int height, final int oldWidth,
                                       final int oldHeight) {
        super.onSizeChanged(width, height, oldWidth, oldHeight);
        circularProgressDrawable
                .setBounds(getPaddingLeft(), getPaddingTop(), width - getPaddingRight(),
                        height - getPaddingBottom());
    }

    @Override
    protected final boolean verifyDrawable(@NonNull final Drawable drawable) {
        return drawable == circularProgressDrawable || super.verifyDrawable(drawable);
    }

}


/*
 * Copyright 2015 - 2019 Michael Rapp
 *
 * 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 de.mrapp.android.view.drawable;

import android.animation.Animator;
import android.animation.Animator.AnimatorListener;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.util.Property;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.LinearInterpolator;

import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import de.mrapp.android.view.CircularProgressBar;
import de.mrapp.util.Condition;

/**
 * An animated drawable, which is used by the view {@link CircularProgressBar}.
 *
 * @author Michael Rapp
 * @since 1.0.0
 */
public class CircularProgressDrawable extends Drawable implements Animatable {

    /**
     * The duration of the angle animation in milliseconds.
     */
    private static final long ANGLE_ANIMATION_DURATION = 2000L;

    /**
     * The duration of the sweep animation in milliseconds.
     */
    private static final long SWEEP_ANIMATION_DURATION = 600L;

    /**
     * The minimum angle of the sweep animation.
     */
    private static final int MIN_SWEEP_ANGLE = 30;

    /**
     * The number of degrees in a circle.
     */
    private static final int MAX_DEGREES = 360;

    /**
     * The color of the progress drawable.
     */
    private final int color;

    /**
     * The thickness of the progress drawable.
     */
    private final int thickness;

    /**
     * The paint, which is used for drawing.
     */
    private Paint paint;

    /**
     * The bounds of the drawable.
     */
    private RectF bounds;

    /**
     * The sweep animator.
     */
    private ObjectAnimator sweepAnimator;

    /**
     * The angle animator.
     */
    private ObjectAnimator angleAnimator;

    /**
     * The current angle of the sweep animation.
     */
    private float currentSweepAngle;

    /**
     * The current angle of the angle animation.
     */
    private float currentGlobalAngle;

    /**
     * The current offset of the angle animation.
     */
    private float currentGlobalAngleOffset;

    /**
     * True, if the progress bar is currently animated to be appearing, false. This value will
     * toggle each time the animation is repeated.
     */
    private boolean appearing;

    /**
     * Initializes the paint, which is used for drawing.
     */
    private void initializePaint() {
        paint = new Paint();
        paint.setAntiAlias(true);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(getThickness());
        paint.setColor(getColor());
    }

    /**
     * Initializes the animators.
     */
    private void initializeAnimators() {
        initializeAngleAnimator();
        initializeSweepAnimator();
    }

    /**
     * Initializes the angle animator.
     */
    private void initializeAngleAnimator() {
        angleAnimator = ObjectAnimator.ofFloat(this, createAngleProperty(), MAX_DEGREES);
        angleAnimator.setInterpolator(new LinearInterpolator());
        angleAnimator.setDuration(ANGLE_ANIMATION_DURATION);
        angleAnimator.setRepeatMode(ValueAnimator.RESTART);
        angleAnimator.setRepeatCount(ValueAnimator.INFINITE);
    }

    /**
     * Creates and returns a property, which allows to animate the global angle of the progress
     * drawable.
     *
     * @return The property, which has been created, as an instance of the class {@link Property}
     */
    private Property<CircularProgressDrawable, Float> createAngleProperty() {
        return new Property<CircularProgressDrawable, Float>(Float.class, "angle") {

            @Override
            public Float get(final CircularProgressDrawable object) {
                return currentGlobalAngle;
            }

            @Override
            public void set(final CircularProgressDrawable object, final Float value) {
                currentGlobalAngle = value;
                invalidateSelf();
            }

        };
    }

    /**
     * Initializes the sweep animator.
     */
    private void initializeSweepAnimator() {
        sweepAnimator = ObjectAnimator
                .ofFloat(this, createSweepProperty(), MAX_DEGREES - MIN_SWEEP_ANGLE * 2);
        sweepAnimator.setInterpolator(new DecelerateInterpolator());
        sweepAnimator.setDuration(SWEEP_ANIMATION_DURATION);
        sweepAnimator.setRepeatMode(ValueAnimator.RESTART);
        sweepAnimator.setRepeatCount(ValueAnimator.INFINITE);
        sweepAnimator.addListener(createSweepAnimatorListener());
    }

    /**
     * Creates and returns a property, which allows to animate the sweep angle of the progress
     * drawable.
     *
     * @return The property, which has been created, as an instance of the class {@link Property}
     */
    private Property<CircularProgressDrawable, Float> createSweepProperty() {
        return new Property<CircularProgressDrawable, Float>(Float.class, "arc") {

            @Override
            public Float get(final CircularProgressDrawable object) {
                return currentSweepAngle;
            }

            @Override
            public void set(final CircularProgressDrawable object, final Float value) {
                currentSweepAngle = value;
                invalidateSelf();
            }

        };
    }

    /**
     * Creates and returns a listener, which allows to restart the progress drawable's animation,
     * when it has been finished.
     *
     * @return The listener, which has been created, as an instance of the type {@link
     * AnimatorListener}
     */
    private AnimatorListener createSweepAnimatorListener() {
        return new AnimatorListener() {

            @Override
            public void onAnimationStart(final Animator animation) {

            }

            @Override
            public void onAnimationEnd(final Animator animation) {

            }

            @Override
            public void onAnimationCancel(final Animator animation) {

            }

            @Override
            public void onAnimationRepeat(final Animator animation) {
                appearing = !appearing;

                if (appearing) {
                    currentGlobalAngleOffset =
                            (currentGlobalAngleOffset + MIN_SWEEP_ANGLE * 2) % MAX_DEGREES;
                }
            }

        };
    }

    /**
     * Creates a new animated drawable, which is used by the view {@link CircularProgressBar}.
     *
     * @param color
     *         The color of the progress drawable as an {@link Integer} value
     * @param thickness
     *         The thickness of the progress drawable as an {@link Integer} value in pixels
     */
    public CircularProgressDrawable(@ColorInt final int color, final int thickness) {
        Condition.INSTANCE.ensureAtLeast(thickness, 1, "The thickness must be at least 1");
        this.color = color;
        this.thickness = thickness;
        this.bounds = new RectF();
        initializePaint();
        initializeAnimators();
    }

    /**
     * Returns the color of the progress drawable.
     *
     * @return The color of the progress drawable as an {@link Integer} value
     */
    public final int getColor() {
        return color;
    }

    /**
     * Returns the thickness of the progress drawable.
     *
     * @return The thickness of the progress drawable in pixels as an {@link Integer}
     */
    public final int getThickness() {
        return thickness;
    }

    @Override
    public final void setAlpha(final int alpha) {
        paint.setAlpha(alpha);
    }

    @Override
    public final void setColorFilter(final ColorFilter cf) {
        paint.setColorFilter(cf);
    }

    @Override
    public final int getOpacity() {
        return PixelFormat.TRANSPARENT;
    }

    @Override
    public final void draw(@NonNull final Canvas canvas) {
        float startAngle = currentGlobalAngle - currentGlobalAngleOffset;
        float sweepAngle = currentSweepAngle;

        if (!appearing) {
            startAngle = startAngle + sweepAngle;
            sweepAngle = MAX_DEGREES - sweepAngle - MIN_SWEEP_ANGLE;
        } else {
            sweepAngle += MIN_SWEEP_ANGLE;
        }

        canvas.drawArc(bounds, startAngle, sweepAngle, false, paint);
    }

    @Override
    public final void start() {
        if (!isRunning()) {
            angleAnimator.start();
            sweepAnimator.start();
            invalidateSelf();
        }
    }

    @Override
    public final void stop() {
        if (isRunning()) {
            angleAnimator.cancel();
            sweepAnimator.cancel();
            invalidateSelf();
        }
    }

    @Override
    public final boolean isRunning() {
        return angleAnimator.isRunning();
    }

    @Override
    protected final void onBoundsChange(final Rect bounds) {
        super.onBoundsChange(bounds);
        int width = bounds.right - bounds.left;
        int height = bounds.bottom - bounds.top;

        if (width < height) {
            int diff = height - width;
            this.bounds.left = bounds.left + thickness / 2.0f + 0.5f;
            this.bounds.right = bounds.right - thickness / 2.0f - 0.5f;
            this.bounds.top = bounds.top + diff / 2.0f + thickness / 2.0f + 0.5f;
            this.bounds.bottom = bounds.bottom - diff / 2.0f - thickness / 2.0f - 0.5f;
        } else {
            int diff = width - height;
            this.bounds.left = bounds.left + diff / 2.0f + thickness / 2.0f + 0.5f;
            this.bounds.right = bounds.right - diff / 2.0f - thickness / 2.0f - 0.5f;
            this.bounds.top = bounds.top + thickness / 2.0f + 0.5f;
            this.bounds.bottom = bounds.bottom - thickness / 2.0f - 0.5f;
        }
    }

}


/*
 * Copyright 2015 - 2019 Michael Rapp
 *
 * 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 de.mrapp.android.view.example;

import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.Toast;

import de.mrapp.android.view.Chip;
import de.mrapp.android.view.FloatingActionButton;

/**
 * The example app's main activity.
 *
 * @author Michael Rapp
 */
public class MainActivity extends AppCompatActivity {

    /**
     * Then name of the extra, which is used to store, whether the floating action buttons are
     * currently visible, within a bundle.
     */
    private static final String FLOATING_ACTION_BUTTONS_VISIBLE_EXTRA =
            MainActivity.class.getSimpleName() + "FloatingActionButtonsVisible";

    /**
     * The small sized floating action button.
     */
    private FloatingActionButton smallFloatingActionButton;

    /**
     * The normal sized floating action button.
     */
    private FloatingActionButton normalFloatingActionButton;

    /**
     * The large sized floating action button.
     */
    private FloatingActionButton largeFloatingActionButton;

    /**
     * True, if the floating action buttons are currently visible, false otherwise.
     */
    private boolean floatingActionButtonsVisible = true;

    /**
     * Initializes the floating action buttons.
     */
    private void initializeFloatingActionButtons() {
        smallFloatingActionButton = findViewById(R.id.floating_action_button_small);
        smallFloatingActionButton.setOnClickListener(createFloatingActionButtonListener());
        normalFloatingActionButton = findViewById(R.id.floating_action_button_normal);
        normalFloatingActionButton.setOnClickListener(createFloatingActionButtonListener());
        largeFloatingActionButton = findViewById(R.id.floating_action_button_large);
        largeFloatingActionButton.setOnClickListener(createFloatingActionButtonListener());
    }

    /**
     * Creates and returns a listener, which allows to show a toast when a floating action button
     * has been clicked.
     *
     * @return The listener, which has been created, as an instance of the type {@link
     * OnClickListener}
     */
    private OnClickListener createFloatingActionButtonListener() {
        return new OnClickListener() {

            @Override
            public void onClick(final View v) {
                Toast toast =
                        Toast.makeText(MainActivity.this, R.string.floating_action_button_toast,
                                Toast.LENGTH_SHORT);
                toast.show();
            }

        };
    }

    /**
     * Initializes the button, which allows to show or hide the floating action buttons.
     */
    private void initializeHideFloatingActionButtonsButton() {
        Button button = findViewById(R.id.hide_floating_action_buttons);
        button.setOnClickListener(createHideFloatingActionButtonsListener());
    }

    /**
     * Creates and returns a listener, which allows to show or hide the floating action buttons.
     *
     * @return The listener, which has been created, as an instance of the type {@link
     * OnClickListener}
     */
    private OnClickListener createHideFloatingActionButtonsListener() {
        return new OnClickListener() {

            @Override
            public void onClick(final View v) {
                int visibility = floatingActionButtonsVisible ? View.INVISIBLE : View.VISIBLE;
                smallFloatingActionButton.setVisibility(visibility, true);
                normalFloatingActionButton.setVisibility(visibility, true);
                largeFloatingActionButton.setVisibility(visibility, true);
                floatingActionButtonsVisible = !floatingActionButtonsVisible;
            }

        };
    }

    /**
     * Initializes all closable chips.
     */
    private void initializeChips() {
        Chip closableChipLight = findViewById(R.id.chip_closable_light);
        closableChipLight.addCloseListener(createChipCloseListener());
        Chip closableChipDark = findViewById(R.id.chip_closable_dark);
        closableChipDark.addCloseListener(createChipCloseListener());
        Chip closableIconChipLight = findViewById(R.id.chip_icon_closable_light);
        closableIconChipLight.addCloseListener(createChipCloseListener());
        Chip closableIconChipDark = findViewById(R.id.chip_icon_closable_dark);
        closableIconChipDark.addCloseListener(createChipCloseListener());
    }

    /**
     * Creates and returns a listener, which allows to show a toast, when a chip has been closed.
     *
     * @return The listener, which has been created, as an instance of the type {@link
     * Chip.CloseListener}
     */
    private Chip.CloseListener createChipCloseListener() {
        return new Chip.CloseListener() {

            @Override
            public void onChipClosed(@NonNull final Chip chip) {
                chip.setVisibility(View.GONE);
                Toast.makeText(MainActivity.this, R.string.chip_closed_toast, Toast.LENGTH_SHORT)
                        .show();
            }

        };
    }

    /**
     * Adapts the activity's views depending on the saved instance state, which has been passed to
     * the activity.
     *
     * @param savedInstanceState
     *         The saved instance state as an instance of the class {@link Bundle} or null, if no
     *         saved instance state is available
     */
    private void handleSavedInstanceState(@Nullable final Bundle savedInstanceState) {
        if (savedInstanceState != null) {
            floatingActionButtonsVisible =
                    savedInstanceState.getBoolean(FLOATING_ACTION_BUTTONS_VISIBLE_EXTRA);
            int visibility = floatingActionButtonsVisible ? View.VISIBLE : View.INVISIBLE;
            smallFloatingActionButton.setVisibility(visibility);
            normalFloatingActionButton.setVisibility(visibility);
            largeFloatingActionButton.setVisibility(visibility);
        }
    }

    @Override
    protected final void onCreate(final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initializeFloatingActionButtons();
        initializeHideFloatingActionButtonsButton();
        initializeChips();
        handleSavedInstanceState(savedInstanceState);
    }

    @Override
    protected final void onSaveInstanceState(final Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putBoolean(FLOATING_ACTION_BUTTONS_VISIBLE_EXTRA, floatingActionButtonsVisible);
    }

}