/**
 * Copyright (C) 2012-2019 the original author or authors.
 *
 * 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 ninja;

import java.io.IOException;
import java.io.OutputStream;
import java.io.Writer;
import java.util.AbstractMap;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;

import ninja.exceptions.InternalServerErrorException;
import ninja.utils.DateUtil;
import ninja.utils.NoHttpBody;
import ninja.utils.ResponseStreams;
import ninja.utils.SwissKnife;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

public class Result {

    private final Logger logger = LoggerFactory.getLogger(Result.class);

    // /////////////////////////////////////////////////////////////////////////
    // HTTP Status codes (for convenience)
    // /////////////////////////////////////////////////////////////////////////
    public static final int SC_101_SWITCHING_PROTOCOLS = 101;
    public static final int SC_200_OK = 200;
    public static final int SC_201_CREATED = 201;
    public static final int SC_204_NO_CONTENT = 204;

    // for redirects:
    public static final int SC_300_MULTIPLE_CHOICES = 300;
    public static final int SC_301_MOVED_PERMANENTLY = 301;
    public static final int SC_302_FOUND = 302;
    public static final int SC_303_SEE_OTHER = 303;
    public static final int SC_304_NOT_MODIFIED = 304;
    public static final int SC_307_TEMPORARY_REDIRECT = 307;

    public static final int SC_400_BAD_REQUEST = 400;
    public static final int SC_401_UNAUTHORIZED = 401;
    public static final int SC_403_FORBIDDEN = 403;
    public static final int SC_404_NOT_FOUND = 404;

    public static final int SC_500_INTERNAL_SERVER_ERROR = 500;
    public static final int SC_501_NOT_IMPLEMENTED = 501;

    // /////////////////////////////////////////////////////////////////////////
    // Some MIME types (for convenience)
    // /////////////////////////////////////////////////////////////////////////
    public static final String TEXT_HTML = "text/html";
    public static final String TEXT_PLAIN = "text/plain";
    public static final String APPLICATION_JSON = "application/json";
    /* @deprecated Naming mistake - Please use APPLICATION_JSON instead! */
    @Deprecated
    public static final String APPLICATON_JSON = APPLICATION_JSON;
    public static final String APPLICATION_JSONP = "application/javascript";
    /* @deprecated Naming mistake - Please use APPLICATION_JSONP instead! */
    @Deprecated
    public static final String APPLICATON_JSONP = APPLICATION_JSONP;
    public static final String APPLICATION_XML = "application/xml";
    public static final String APPLICATION_OCTET_STREAM = "application/octet-stream";

    // This is a marker. Returning something like
    // Result.html().render(NO_HTTP_BODY) will cause all body rendering
    // to be bypassed. This effectively means you'll return only headers and no body
    // useful for "no content" style responses.
    public static final NoHttpBody NO_HTTP_BODY = new NoHttpBody();

    // /////////////////////////////////////////////////////////////////////////
    // Finally we got to the core of this class...
    // /////////////////////////////////////////////////////////////////////////
    /* Used as redirection header */
    public static final String LOCATION = "Location";
    public static final String CACHE_CONTROL = "Cache-Control";
    public static final String CACHE_CONTROL_DEFAULT_NOCACHE_VALUE = "no-cache, no-store, max-age=0, must-revalidate";

    public static final String DATE = "Date";
    public static final String EXPIRES = "Expires";

    public static final String WWW_AUTHENTICATE = "WWW-Authenticate";

    private int statusCode;

    private Class<?> jsonView;

    /* The object that will be rendered. Could be a Java Pojo. Or a map. Or xyz. Will be
     * handled by the TemplateReneringEngine. */
    private Object renderable;

    /**
     * Something like: "text/html" or "application/json"
     */
    private String contentType;

    /**
     * If content type is not set AND supported type does not match accept
     * header of request this fallback will be used. If it is not set
     * a bad request exception will be thrown.
     */
    private Optional<String> fallbackContentType = Optional.empty();

    /**
     * A list of content types this result will handle. If you got a general
     * person object you can render it via application/json and application/xml
     * without changing anything inside your controller for instance.
     *
     */
    private final List<String> supportedContentTypes = Lists.newArrayList();

    /**
     * A newly created Result will handle those three result types out of
     * the box.
     */
    private final static List<String> DEFAULT_SUPPORTED_CONTENT_TYPES
            = ImmutableList.of(TEXT_HTML, APPLICATION_JSON, APPLICATION_XML);

    /**
     * Something like: "utf-8" => will be appended to the content-type. eg
     * "text/html; charset=utf-8"
     */
    private String charset;

    private Map<String, String> headers;

    private List<Cookie> cookies;

    private String template;

    /**
     * A result. Sets utf-8 as charset and status code by default.
     * Refer to {@link Result#SC_200_OK}, {@link Result#SC_204_NO_CONTENT} and so on
     * for some short cuts to predefined results.
     *
     * @param statusCode The status code to set for the result.
     *                    Shortcuts to the code at: {@link Result#SC_200_OK}
     */
    public Result(int statusCode) {

        this.statusCode = statusCode;
        this.charset = "utf-8";

        this.headers = Maps.newHashMap();
        this.cookies = Lists.newArrayList();

    }

    public Object getRenderable() {
        return renderable;
    }

    /**
     * This method handles two principal cases:
     * 1) If the this.renderable of this result is null, the object passed is simply set as renderable
     *    for this Result
     * 2) If the this.renderable of this result is not null an new map is generated as
     *    object to render and both the former renderable and the new object added to the map.
     *    The former object is gets the class name in camelCase as key.
     *
     * If the converted camelCase key of this object already exists an {@link IllegalArgumentException}
     * is being thrown.
     *
     * @param object The object to add (either an arbitrary class or Renderable).
     * @return Result this result for chaining.
     *
     */
    public Result render(Object object) {

        if (this.renderable == null) {

            this.renderable = object;

        } else {

            assertObjectNoRenderableOrThrowException(this.renderable);

            Map<String, Object> map;

            // prepare the this.renderable as we now have at least two
            // objects to render
            if (this.renderable instanceof Map) {
               map = (Map) this.renderable;

            } else {
                map = Maps.newHashMap();
                // add original this.renderable
                map.put(SwissKnife.getRealClassNameLowerCamelCase(this.renderable), this.renderable);
                this.renderable = map;

            }

            // add object of this method
            String key = SwissKnife.getRealClassNameLowerCamelCase(object);
            if (map.containsKey(key)) {
                throw new IllegalArgumentException(
                        String.format(
                              "Cannot store object with default name %s."
                              + "An object with the same name is already stored."
                              + "Consider using render(key, value) to name objects implicitly.",
                              key));

            } else {
                map.put(SwissKnife.getRealClassNameLowerCamelCase(object), object);
            }

        }

        return this;
    }

    /**
     * Replaces the object being passed by this result to the rendering engine
     * with this map. It will overwrite any previously set render(...) calls.
     *
     * @param mapToRender The map being passed to the templating engine.
     * @return This Result for chaining.
     */
    public Result render(Map<String, Object> mapToRender) {
        this.renderable = mapToRender;
        return this;
    }

    /**
     * Handles following cases:
     * 1) If this.renderable is null: a new HashMap is generated and this entry being added
     *    to the map.
     * 2) If this.renderable is a Map: the entry is added
     * 3) If this.renderable is an object (not a renderable): a Map is generated and both
     *    the former object and the new entry are being added.
     * 4) If this.renderable is a Renderable: an {@link IllegalArgumentException} is thrown.
     *
     * If the entry key already exists in the map of this.renderable an {@link IllegalArgumentException}
     * is thrown.
     *
     * @param entry The entry to add.
     * @return The result for further chaining.
     */
    public Result render(Entry<String, Object> entry) {

        if (this.renderable == null) {

            Map<String, Object> map = Maps.newHashMap();
            this.renderable = map;
            map.put(entry.getKey(), entry.getValue());
        } else {

            assertObjectNoRenderableOrThrowException(this.renderable);

            Map<String, Object> map;
            if (this.renderable instanceof Map) {

                map = (Map) this.renderable;
                if (map.containsKey(entry.getKey())) {
                    throw new IllegalArgumentException(
                           String.format(
                                 "Entry with key %s already stored inside this Result object. "
                                 + "This is currently not supported and does not make sense. "
                                 +  "Consider using your own map.",
                                         entry.getKey()));
                } else {
                    map.put(entry.getKey(), entry.getValue());

                }

            } else {
                map = Maps.newHashMap();
                map.put(
                        SwissKnife.getRealClassNameLowerCamelCase(this.renderable),
                        this.renderable);

                this.renderable = map;

            }

            map.put(entry.getKey(), entry.getValue());

        }

        return this;

    }

    /**
     * Sets this renderable as object to render. Usually this renderable
     * does rendering itself and will not call any templating engine.
     *
     * @param renderable The renderable that will handle everything after returing the result.
     * @return This result for chaining.
     */
    public Result render(Renderable renderable) {
        this.renderable = renderable;
        return this;
    }

    /**
     * Implicitly generates a hashmap as object being rendered and adds
     * this key, value pair. If the object being rendered is already a hashmap
     * it simply adds this key value pair to it.
     *
     * @param key The key to use.
     * @param value The value to use.
     * @return The Result for chaining.
     */
    public Result render(String key, Object value) {

        render(new AbstractMap.SimpleEntry<String, Object>(key, value));

        return this;

    }

    /**
     * This method directly renders the String to the output. It completely
     * bypasses any rendering engine.
     *
     * Thus you can render anything you want.
     *
     * Chaining of resultRaw().resultRaw()... is NOT supported. Mixing with
     * render() is NOT supported.
     *
     * It is always recommended to implement your own RenderingEngine OR use
     * existing rendering engines.
     *
     * Example: <code>
     * public Result controllerMethod() {
     *    String customJson = "{\"user\" : \"john@woo.com\"}";
     *
     *    return Results.json().renderRaw(customJson);
     * }
     * </code>
     *
     * @param string
     *            The string to render.
     * @return A result that will render the string directly to the output
     *         stream.
     * @deprecated => use text().render(string), html().render(string),
     *             json().render(string), xml().render(string), or
     *             contentType(type).render(string).
     */
    @Deprecated
    public Result renderRaw(final String string) {

        Renderable renderable = new Renderable() {

            @Override
            public void render(Context context, Result result) {

                if (result.getContentType() == null) {
                    result.contentType(Result.TEXT_PLAIN);
                }

                ResponseStreams resultJsonCustom = context
                        .finalizeHeaders(result);

                try (Writer writer = resultJsonCustom.getWriter()) {

                    writer.write(string);

                } catch (IOException ioException) {

                    logger.error(
                            "Error rendering raw String via renderRaw(...)",
                            ioException);
                }

            }
        };

        render(renderable);

        return this;

    }

    /**
     * This method directly renders the byte array to the output. It
     * completely bypasses any rendering engine.
     *
     * Thus you can render anything you want.
     *
     * Chaining of resultRaw().resultRaw()... is NOT supported. Mixing with render()
     * is NOT supported.
     *
     * It is always recommended to implement your own RenderingEngine OR
     * use existing rendering engines.
     *
     * @param bytes The bytes to render.
     * @return A result that will render the string directly to the output stream.
     */
    public Result renderRaw(final byte [] bytes) {

        Renderable renderable = new Renderable() {

            @Override
            public void render(Context context, Result result) {
                if (result.getContentType() == null) {
                    result.contentType(Result.APPLICATION_OCTET_STREAM);
                }
                ResponseStreams responseStreams = context
                        .finalizeHeaders(result);

                try (OutputStream outputStream = responseStreams.getOutputStream()) {

                    outputStream.write(bytes);

                } catch (IOException ioException) {
                    throw new InternalServerErrorException(ioException);
                }
            }
        };

        render(renderable);

        return this;

   }

    public String getContentType() {
        return contentType;
    }

    /**
     * @return Charset of the current result that will be used. Will be "utf-8"
     *         by default.
     */
    public String getCharset() {
        return charset;
    }

    /**
     * @param charset Set the charset of the result. Is "utf-8" by default.
     * @return The result for chaining.
     */
    public Result charset(String charset) {
        this.charset = charset;
        return this;
    }

    /**
     * Sets the content type
     *
     * @param contentType
     * @Deprecated => please use shortcut contentType(...)
     */
    @Deprecated
    public Result setContentType(String contentType) {
        this.contentType = contentType;
        return this;
    }

    /**
     * Sets the content type. Must not contain any charset WRONG:
     * "text/html; charset=utf8".
     *
     * If you want to set the charset use method {@link Result#charset(String)};
     *
     * @param contentType
     *            (without encoding) something like "text/html" or
     *            "application/json"
     * @return The result for chaining.
     */
    public Result contentType(String contentType) {
        this.contentType = contentType;
        return this;
    }

    /**
     * Will add a content type to the list of supported content types.
     * Calling that method two times with different content types will add both
     * content types.
     *
     * @param contentTypeSupportedByThisResult The content type to add. Eg. "application/xml"
     * @return The result for chaining.
     */
    public Result supportedContentType(String contentTypeSupportedByThisResult) {
        supportedContentTypes.add(contentTypeSupportedByThisResult);
        return this;
    }

    /**
     * Will add the content types to the list of supported content types.
     *
     * @param contentTypesSupportedByThisResult The content type to add. Eg.
     *        "application/xml", "applcation/json"
     * @return The result for chaining.
     */
    public Result supportedContentTypes(String ... contentTypesSupportedByThisResult) {
        supportedContentTypes.addAll(Arrays.asList(contentTypesSupportedByThisResult));
        return this;
    }

    /**
     * Returns immutable list of supported content types by this request.
     *
     * @return immutable list of supported content types. Either the default
         content types if no one has been set (html, json, xml) or the
         content types set by the user and supportedContentType(...).
     */
    public List<String> supportedContentTypes() {
        if (supportedContentTypes.isEmpty()) {
            return DEFAULT_SUPPORTED_CONTENT_TYPES;
        } else {
            return ImmutableList.copyOf(supportedContentTypes);
        }
    }

    /**
     *
     * @return The fallback content type. This will be the content type used
     * when none of the supported content types matches the accept content
     * type of the request.
     */
    public Optional<String> fallbackContentType() {
        return fallbackContentType;
    }

    /**
     *
     * @param fallbackContentType The content type to use as fallback when
     *                            neither contentType set and supportedContentTypes
     *                            do not match request.
     * @return This result for chaining.
     */
    public Result fallbackContentType(String fallbackContentType) {
        this.fallbackContentType = Optional.of(fallbackContentType);
        return this;
    }

    public Map<String, String> getHeaders() {
        return headers;
    }

    public Result addHeader(String headerName, String headerContent) {

        headers.put(headerName, headerContent);
        return this;
    }

    /**
     * Returns cookie with that name or null.
     *
     * @param cookieName Name of the cookie
     * @return The cookie or null if not found.
     */
    public Cookie getCookie(String cookieName) {

        for (Cookie cookie : getCookies()) {
            if (cookie.getName().equals(cookieName)) {
                return cookie;
            }
        }

        return null;
    }

    public List<Cookie> getCookies() {
        return cookies;
    }

    public Result addCookie(Cookie cookie) {
        cookies.add(cookie);
        return this;
    }

    public Result unsetCookie(String name) {
        cookies.add(Cookie.builder(name, "").setMaxAge(0).build());
        return this;
    }

    public int getStatusCode() {
        return statusCode;
    }

    /**
     * Set the status of this result.
     * Refer to {@link Result#SC_200_OK}, {@link Result#SC_204_NO_CONTENT} and so on
     * for some short cuts to predefined results.
     *
     * @param statusCode The status code. Result ({@link Result#SC_200_OK}) provides some helpers.
     * @return The result you executed the method on for method chaining.
     */
    public Result status(int statusCode) {
        this.statusCode = statusCode;
        return this;
    }

    public String getTemplate() {
        return template;
    }

    /**
     * Set the template to render. For instance
     * template("views/AnotherController/anotherview.ftl.html");
     *
     * @param template The view to render. Eg. views/AnotherController/anotherview.ftl.html
     * @return The result that you executed the method on for chaining.
     */
    public Result template(String template) {
        this.template = template;
        return this;
    }

    public Class<?> getJsonView() {
        return jsonView;
    }

    /**
     * Set the Jackson JSON View.
     * See <a href="http://wiki.fasterxml.com/JacksonJsonViews">http://wiki.fasterxml.com/JacksonJsonViews</a>
     *
     * @param jsonView JSON serialization view class to use when rendering
     * @return The result that you executed the method on for chaining, with JSON view set
     */
    public Result jsonView(Class<?> jsonView) {
        this.jsonView = jsonView;
        return this;
    }

    /**
     * A redirect that uses 303 see other.
     *
     * @param url
     *            The url used as redirect target.
     * @return A nicely configured result with status code 303 and the url set
     *         as Location header.
     */
    public Result redirect(String url) {

        status(Result.SC_303_SEE_OTHER);
        addHeader(Result.LOCATION, url);

        return this;
    }

    /**
     * A redirect that uses 307 see other.
     *
     * @param url
     *            The url used as redirect target.
     * @return A nicely configured result with status code 307 and the url set
     *         as Location header.
     */
    public Result redirectTemporary(String url) {

        status(Result.SC_307_TEMPORARY_REDIRECT);
        addHeader(Result.LOCATION, url);

        return this;
    }
    /**
     * Set the content type of this result to {@link Result#TEXT_HTML}.
     *
     * @return the same result where you executed this method on. But the content type is now {@link Result#TEXT_HTML}.
     */
    public Result html() {
        contentType = TEXT_HTML;
        return this;
    }

    /**
     * Set the content type of this result to {@link Result#APPLICATION_JSON}.
     *
     * @return the same result where you executed this method on. But the content type is now {@link Result#APPLICATION_JSON}.
     */
    public Result json() {
        contentType = APPLICATION_JSON;
        return this;
    }

    /**
     * Set the content type of this result to {@link Result#APPLICATION_JSONP}.
     *
     * @return the same result where you executed this method on. But the content type is now {@link Result#APPLICATION_JSONP}.
     */
    public Result jsonp() {
        contentType = APPLICATION_JSONP;
        return this;
    }

    /**
     * Set the content type of this result to {@link Result#TEXT_PLAIN}.
     *
     * @return the same result where you executed this method on. But the content type is now {@link Result#TEXT_PLAIN}.
     */
    public Result text() {
        contentType = TEXT_PLAIN;
        return this;
    }

    /**
     * Set the content type of this result to {@link Result#APPLICATON_XML}.
     *
     * @return the same result where you executed this method on. But the content type is now {@link Result#APPLICATON_XML}.
     */
    public Result xml() {
        contentType = APPLICATION_XML;
        return this;
    }

    /**
     * This function sets
     *
     * Cache-Control: no-cache, no-store
     * Date: (current date)
     * Expires: 1970
     *
     * => it therefore effectively forces the browser and every proxy in between
     * not to cache content.
     *
     * See also https://devcenter.heroku.com/articles/increasing-application-performance-with-http-cache-headers
     *
     * @return this result for chaining.
     */
    public Result doNotCacheContent() {

        addHeader(CACHE_CONTROL, CACHE_CONTROL_DEFAULT_NOCACHE_VALUE);
        addHeader(DATE, DateUtil.formatForHttpHeader(System.currentTimeMillis()));
        addHeader(EXPIRES, DateUtil.formatForHttpHeader(0L));

        return this;

    }

    private void assertObjectNoRenderableOrThrowException(Object object) {
        if (object instanceof Renderable) {
            throw new IllegalArgumentException(
                    "You already want to render a Renderable class. " +
                "Adding more items to render is not supported.");

        }
    }

}


/**
 * Copyright (C) 2012-2019 the original author or authors.
 *
 * 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 ninja.utils;

import java.io.File;
import java.io.IOException;

import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.io.Files;

public class NinjaPropertiesImplTool {

    private static final Logger logger = LoggerFactory
            .getLogger(NinjaPropertiesImplTool.class);

    /**
     * This method checks that your configurations have set a
     * application.secret=23r213r12r123
     *
     * If application.secret is missing or is empty it will do the following:
     * - In dev and test mode it'll generate a new application secret and write the secret
     *   to both src/main/java/conf/application.conf and the classes dir were the compiled stuff
     *   goes.
     * - In prod it will throw a runtime exception and stop the server.
     */
    public static void checkThatApplicationSecretIsSet(
                                                boolean isProd,
                                                String baseDirWithoutTrailingSlash,
                                                PropertiesConfiguration defaultConfiguration,
                                                Configuration compositeConfiguration) {

        String applicationSecret = compositeConfiguration.getString(NinjaConstant.applicationSecret);

        if (applicationSecret == null
                || applicationSecret.isEmpty()) {

            // If in production we stop the startup process. It simply does not make
            // sense to run in production if the secret is not set.
            if (isProd) {
                String errorMessage = "Fatal error. Key application.secret not set. Please fix that.";
                logger.error(errorMessage);
                throw new RuntimeException(errorMessage);
            }

            logger.info("Key application.secret not set. Generating new one and setting in conf/application.conf.");

            // generate new secret
            String secret = SecretGenerator.generateSecret();

            // set in overall composite configuration => this enables this instance to
            // start immediately. Otherwise we would have another build cycle.
            compositeConfiguration.setProperty(NinjaConstant.applicationSecret, secret);

            // defaultConfiguration is: conf/application.conf (not prefixed)
            defaultConfiguration.setProperty(NinjaConstant.applicationSecret, secret);

            try {

                // STEP 1: Save in source directories:
                // save to compiled version => in src/main/target/
                String pathToApplicationConfInSrcDir = baseDirWithoutTrailingSlash + File.separator + "src" + File.separator + "main" + File.separator + "java" + File.separator + NinjaProperties.CONF_FILE_LOCATION_BY_CONVENTION;
                Files.createParentDirs(new File(pathToApplicationConfInSrcDir));
                // save to source
                defaultConfiguration.save(pathToApplicationConfInSrcDir);

                // STEP 2: Save in classes dir (target/classes or similar).
                // save in target directory (compiled version aka war aka classes dir)
                defaultConfiguration.save();

            } catch (ConfigurationException e) {
                logger.error("Error while saving new secret to application.conf.", e);
            } catch (IOException e) {
                logger.error("Error while saving new secret to application.conf.", e);
            }

        }

    }

}


/**
 * Copyright (C) 2012-2019 the original author or authors.
 *
 * 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 ninja;

import java.io.File;
import java.net.URISyntaxException;
import java.net.URL;

import org.apache.commons.io.FilenameUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.apache.commons.lang.StringUtils;

public class AssetsControllerHelper {

    private final static Logger logger = LoggerFactory
            .getLogger(AssetsControllerHelper.class);

    /**
     * If we get - for whatever reason - a relative URL like
     * assets/../conf/application.conf we expand that to the "real" path. In the
     * above case conf/application.conf.
     *
     * You should then add the assets prefix.
     *
     * Otherwise someone can create an attack and read all resources of our app.
     * If we expand and normalize the incoming path this is no longer possible.
     *
     * @param fileName A potential "fileName"
     * @param enforceUnixSeparator If true it will force the usage of the unix separator '/'
     *                             If false it will use the separator of the underlying system.
     *                             usually '/' in case of unix and '\' in case of windows.
     * @return A normalized fileName.
     */
    public String normalizePathWithoutLeadingSlash(String fileName, boolean enforceUnixSeparator) {
        String fileNameNormalized = enforceUnixSeparator
                ? FilenameUtils.normalize(fileName, true)
                : FilenameUtils.normalize(fileName);
        return StringUtils.removeStart(fileNameNormalized, "/");
    }

    /**
     * Check the URL is a directory.
     * With war style deployment, AssetsController exposes the file list of assets directories.
     * For example, a request to http://localhost:8080/assets/css/ displays the file list of css directory.
     * So this method checks the URL is a directory.
     *
     * @param url A URL of assets
     * @return true if the URL is a directory
     */
    public boolean isDirectoryURL(URL url) {
        try {
            return url.getProtocol().equals("file") && new File(url.toURI()).isDirectory();
        } catch (URISyntaxException e) {
            logger.error("Could not URL convert to URI", e);
        }
        return false;
    }

}


/*
 * Copyright 2014 Niek Haarman
 *
 * 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.nhaarman.listviewanimations.appearance;

import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;

import com.nhaarman.listviewanimations.BaseAdapterDecorator;
import com.nhaarman.listviewanimations.util.AnimatorUtil;
import com.nhaarman.listviewanimations.util.ListViewWrapper;
import com.nhaarman.listviewanimations.util.StickyListHeadersListViewWrapper;
import com.nineoldandroids.animation.Animator;
import com.nineoldandroids.animation.ObjectAnimator;

import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter;
import se.emilsjolander.stickylistheaders.StickyListHeadersListView;

/**
 * A {@link com.nhaarman.listviewanimations.BaseAdapterDecorator} which can be used to animate header views provided by a
 * {@link se.emilsjolander.stickylistheaders.StickyListHeadersAdapter}.
 */
public class StickyListHeadersAdapterDecorator extends BaseAdapterDecorator implements StickyListHeadersAdapter {

    /**
     * Alpha property.
     */
    private static final String ALPHA = "alpha";

    /**
     * The decorated {@link se.emilsjolander.stickylistheaders.StickyListHeadersAdapter}.
     */
    @NonNull
    private final StickyListHeadersAdapter mStickyListHeadersAdapter;

    /**
     * The {@link com.nhaarman.listviewanimations.appearance.ViewAnimator} responsible for animating the Views.
     */
    @Nullable
    private ViewAnimator mViewAnimator;

    /**
     * Create a new {@code StickyListHeadersAdapterDecorator}, decorating given {@link android.widget.BaseAdapter}.
     *
     * @param baseAdapter the {@code BaseAdapter} to decorate. If this is a {@code BaseAdapterDecorator}, it should wrap an instance of
     *                    {@link se.emilsjolander.stickylistheaders.StickyListHeadersAdapter}.
     */
    public StickyListHeadersAdapterDecorator(@NonNull final BaseAdapter baseAdapter) {
        super(baseAdapter);

        BaseAdapter adapter = baseAdapter;
        while (adapter instanceof BaseAdapterDecorator) {
            adapter = ((BaseAdapterDecorator) adapter).getDecoratedBaseAdapter();
        }

        if (!(adapter instanceof StickyListHeadersAdapter)) {
            throw new IllegalArgumentException(adapter.getClass().getCanonicalName() + " does not implement StickyListHeadersAdapter");
        }

        mStickyListHeadersAdapter = (StickyListHeadersAdapter) adapter;
    }

    /**
     * Sets the {@link se.emilsjolander.stickylistheaders.StickyListHeadersListView} that this adapter will be bound to.
     */
    public void setStickyListHeadersListView(@NonNull final StickyListHeadersListView listView) {
        ListViewWrapper stickyListHeadersListViewWrapper = new StickyListHeadersListViewWrapper(listView);
        setListViewWrapper(stickyListHeadersListViewWrapper);
    }

    /**
     * Returns the {@link com.nhaarman.listviewanimations.appearance.ViewAnimator} responsible for animating the header Views in this adapter.
     */
    @Nullable
    public ViewAnimator getViewAnimator() {
        return mViewAnimator;
    }

    @Override
    public void setListViewWrapper(@NonNull final ListViewWrapper listViewWrapper) {
        super.setListViewWrapper(listViewWrapper);
        mViewAnimator = new ViewAnimator(listViewWrapper);
    }

    @Override
    public View getHeaderView(final int position, final View convertView, final ViewGroup parent) {
        if (getListViewWrapper() == null) {
            throw new IllegalStateException("Call setStickyListHeadersListView() on this AnimationAdapter first!");
        }

        if (convertView != null) {
            assert mViewAnimator != null;
            mViewAnimator.cancelExistingAnimation(convertView);
        }

        View itemView = mStickyListHeadersAdapter.getHeaderView(position, convertView, parent);

        animateViewIfNecessary(position, itemView, parent);
        return itemView;
    }

    /**
     * Animates given View if necessary.
     *
     * @param position the position of the item the View represents.
     * @param view     the View that should be animated.
     * @param parent   the parent the View is hosted in.
     */
    private void animateViewIfNecessary(final int position, @NonNull final View view, @NonNull final ViewGroup parent) {
        Animator[] childAnimators;
        if (getDecoratedBaseAdapter() instanceof AnimationAdapter) {
            childAnimators = ((AnimationAdapter) getDecoratedBaseAdapter()).getAnimators(parent, view);
        } else {
            childAnimators = new Animator[0];
        }
        Animator alphaAnimator = ObjectAnimator.ofFloat(view, ALPHA, 0, 1);

        assert mViewAnimator != null;
        mViewAnimator.animateViewIfNecessary(position, view, AnimatorUtil.concatAnimators(childAnimators, new Animator[0], alphaAnimator));
    }

    @Override
    public long getHeaderId(final int position) {
        return mStickyListHeadersAdapter.getHeaderId(position);
    }
}


/*
 * Copyright 2014 Niek Haarman
 *
 * 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.nhaarman.listviewanimations.itemmanipulation.swipedismiss.undo;

import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;

import com.nhaarman.listviewanimations.BaseAdapterDecorator;
import com.nhaarman.listviewanimations.itemmanipulation.DynamicListView;
import com.nhaarman.listviewanimations.itemmanipulation.swipedismiss.DismissableManager;
import com.nhaarman.listviewanimations.util.ListViewWrapper;

/**
 * Adds swipe-undo behaviour to the {@link android.widget.AbsListView}, using a {@link SwipeUndoTouchListener}.
 */
public abstract class SwipeUndoAdapter extends BaseAdapterDecorator {

    /**
     * The {@link SwipeUndoTouchListener} that is set to the {@link android.widget.AbsListView}.
     */
    @Nullable
    private SwipeUndoTouchListener mSwipeUndoTouchListener;

    /**
     * The {@link UndoCallback} that is used.
     */
    @NonNull
    private UndoCallback mUndoCallback;

    /**
     * Create a new {@code SwipeUndoAdapter}, decorating given {@link android.widget.BaseAdapter}.
     *
     * @param baseAdapter  the {@link android.widget.BaseAdapter} to decorate.
     * @param undoCallback the {@link UndoCallback} that is used.
     */
    protected SwipeUndoAdapter(@NonNull final BaseAdapter baseAdapter, @NonNull final UndoCallback undoCallback) {
        super(baseAdapter);
        mUndoCallback = undoCallback;
    }

    @Override
    public void setListViewWrapper(@NonNull final ListViewWrapper listViewWrapper) {
        super.setListViewWrapper(listViewWrapper);
        mSwipeUndoTouchListener = new SwipeUndoTouchListener(listViewWrapper, mUndoCallback);

        if (!(listViewWrapper.getListView() instanceof DynamicListView)) {
            listViewWrapper.getListView().setOnTouchListener(mSwipeUndoTouchListener);
        }
    }

    /**
     * Sets the {@link com.nhaarman.listviewanimations.itemmanipulation.swipedismiss.DismissableManager} to specify which views can or cannot be swiped.
     *
     * @param dismissableManager {@code null} for no restrictions.
     */
    public void setDismissableManager(@Nullable final DismissableManager dismissableManager) {
        if (mSwipeUndoTouchListener == null) {
            throw new IllegalStateException("You must call setAbsListView() first.");
        }
        mSwipeUndoTouchListener.setDismissableManager(dismissableManager);
    }

    public void setSwipeUndoTouchListener(@NonNull final SwipeUndoTouchListener swipeUndoTouchListener) {
        mSwipeUndoTouchListener = swipeUndoTouchListener;
    }

    @NonNull
    @Override
    public View getView(final int position, @Nullable final View convertView, @NonNull final ViewGroup parent) {
        if (getListViewWrapper() == null) {
            throw new IllegalArgumentException("Call setAbsListView() on this SwipeUndoAdapter before setAdapter()!");
        }
        return super.getView(position, convertView, parent);
    }

    /**
     * Sets the {@link UndoCallback} to use.
     */
    public void setUndoCallback(@NonNull final UndoCallback undoCallback) {
        mUndoCallback = undoCallback;
    }

    @NonNull
    public UndoCallback getUndoCallback() {
        return mUndoCallback;
    }

    /**
     * Performs the undo animation and restores the original state for given {@link android.view.View}.
     *
     * @param view the parent {@code View} which contains both primary and undo {@code View}s.
     */
    public void undo(@NonNull final View view) {
        mSwipeUndoTouchListener.undo(view);
    }

    /**
     * Dismisses the {@link android.view.View} corresponding to given position. Calling this method has the same effect as manually swiping an item off the screen.
     *
     * @param position the position of the item in the {@link android.widget.ListAdapter}. Must be visible.
     */
    public void dismiss(final int position) {
        mSwipeUndoTouchListener.dismiss(position);
    }
}


/*
 * Copyright 2014 Niek Haarman
 *
 * 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.nhaarman.listviewanimations.appearance;

import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;

import com.nhaarman.listviewanimations.BaseAdapterDecorator;
import com.nhaarman.listviewanimations.util.AnimatorUtil;
import com.nhaarman.listviewanimations.util.ListViewWrapper;
import com.nineoldandroids.animation.Animator;
import com.nineoldandroids.animation.ObjectAnimator;

/**
 * A {@link BaseAdapterDecorator} class which applies multiple {@link Animator}s at once to views when they are first shown. The Animators applied include the animations specified
 * in {@link #getAnimators(ViewGroup, View)}, plus an alpha transition.
 */
public abstract class AnimationAdapter extends BaseAdapterDecorator {

    /**
     * Saved instance state key for the ViewAniamt
     */
    private static final String SAVEDINSTANCESTATE_VIEWANIMATOR = "savedinstancestate_viewanimator";

    /**
     * Alpha property
     */
    private static final String ALPHA = "alpha";

    /**
     * The ViewAnimator responsible for animating the Views.
     */
    @Nullable
    private ViewAnimator mViewAnimator;

    /**
     * Whether this instance is the root AnimationAdapter. When this is set to false, animation is not applied to the views, since the wrapper AnimationAdapter will take care of
     * that.
     */
    private boolean mIsRootAdapter;

    /**
     * If the AbsListView is an instance of GridView, this boolean indicates whether the GridView is possibly measuring the view.
     */
    private boolean mGridViewPossiblyMeasuring;

    /**
     * The position of the item that the GridView is possibly measuring.
     */
    private int mGridViewMeasuringPosition;

    /**
     * Creates a new AnimationAdapter, wrapping given BaseAdapter.
     *
     * @param baseAdapter the BaseAdapter to wrap.
     */
    protected AnimationAdapter(@NonNull final BaseAdapter baseAdapter) {
        super(baseAdapter);

        mGridViewPossiblyMeasuring = true;
        mGridViewMeasuringPosition = -1;
        mIsRootAdapter = true;

        if (baseAdapter instanceof AnimationAdapter) {
            ((AnimationAdapter) baseAdapter).setIsWrapped();
        }
    }

    @Override
    public void setListViewWrapper(@NonNull final ListViewWrapper listViewWrapper) {
        super.setListViewWrapper(listViewWrapper);
        mViewAnimator = new ViewAnimator(listViewWrapper);
    }

    /**
     * Sets whether this instance is wrapped by another instance of AnimationAdapter. If called, this instance will not apply any animations to the views, since the wrapper
     * AnimationAdapter handles that.
     */
    private void setIsWrapped() {
        mIsRootAdapter = false;
    }

    /**
     * Call this method to reset animation status on all views. The next time {@link #notifyDataSetChanged()} is called on the base adapter, all views will animate again.
     */
    public void reset() {
        if (getListViewWrapper() == null) {
            throw new IllegalStateException("Call setAbsListView() on this AnimationAdapter first!");
        }

        assert mViewAnimator != null;
        mViewAnimator.reset();

        mGridViewPossiblyMeasuring = true;
        mGridViewMeasuringPosition = -1;

        if (getDecoratedBaseAdapter() instanceof AnimationAdapter) {
            ((AnimationAdapter) getDecoratedBaseAdapter()).reset();
        }
    }

    /**
     * Returns the {@link com.nhaarman.listviewanimations.appearance.ViewAnimator} responsible for animating the Views in this adapter.
     */
    @Nullable
    public ViewAnimator getViewAnimator() {
        return mViewAnimator;
    }

    @NonNull
    @Override
    public final View getView(final int position, @Nullable final View convertView, @NonNull final ViewGroup parent) {
        if (mIsRootAdapter) {
            if (getListViewWrapper() == null) {
                throw new IllegalStateException("Call setAbsListView() on this AnimationAdapter first!");
            }

            assert mViewAnimator != null;
            if (convertView != null) {
                mViewAnimator.cancelExistingAnimation(convertView);
            }
        }

        View itemView = super.getView(position, convertView, parent);

        if (mIsRootAdapter) {
            animateViewIfNecessary(position, itemView, parent);
        }
        return itemView;
    }

    /**
     * Animates given View if necessary.
     *
     * @param position the position of the item the View represents.
     * @param view     the View that should be animated.
     * @param parent   the parent the View is hosted in.
     */
    private void animateViewIfNecessary(final int position, @NonNull final View view, @NonNull final ViewGroup parent) {
        assert mViewAnimator != null;

        /* GridView measures the first View which is returned by getView(int, View, ViewGroup), but does not use that View.
           On KitKat, it does this actually multiple times.
           Therefore, we animate all these first Views, and reset the last animated position when we suspect GridView is measuring. */
        mGridViewPossiblyMeasuring = mGridViewPossiblyMeasuring && (mGridViewMeasuringPosition == -1 || mGridViewMeasuringPosition == position);

        if (mGridViewPossiblyMeasuring) {
            mGridViewMeasuringPosition = position;
            mViewAnimator.setLastAnimatedPosition(-1);
        }

        Animator[] childAnimators;
        if (getDecoratedBaseAdapter() instanceof AnimationAdapter) {
            childAnimators = ((AnimationAdapter) getDecoratedBaseAdapter()).getAnimators(parent, view);
        } else {
            childAnimators = new Animator[0];
        }
        Animator[] animators = getAnimators(parent, view);
        Animator alphaAnimator = ObjectAnimator.ofFloat(view, ALPHA, 0, 1);

        Animator[] concatAnimators = AnimatorUtil.concatAnimators(childAnimators, animators, alphaAnimator);
        mViewAnimator.animateViewIfNecessary(position, view, concatAnimators);
    }

    /**
     * Returns the Animators to apply to the views. In addition to the returned Animators, an alpha transition will be applied to the view.
     *
     * @param parent The parent of the view
     * @param view   The view that will be animated, as retrieved by getView().
     */
    @NonNull
    public abstract Animator[] getAnimators(@NonNull ViewGroup parent, @NonNull View view);

    /**
     * Returns a Parcelable object containing the AnimationAdapter's current dynamic state.
     */
    @NonNull
    public Parcelable onSaveInstanceState() {
        Bundle bundle = new Bundle();

        if (mViewAnimator != null) {
            bundle.putParcelable(SAVEDINSTANCESTATE_VIEWANIMATOR, mViewAnimator.onSaveInstanceState());
        }

        return bundle;
    }

    /**
     * Restores this AnimationAdapter's state.
     *
     * @param parcelable the Parcelable object previously returned by {@link #onSaveInstanceState()}.
     */
    public void onRestoreInstanceState(@Nullable final Parcelable parcelable) {
        if (parcelable instanceof Bundle) {
            Bundle bundle = (Bundle) parcelable;
            if (mViewAnimator != null) {
                mViewAnimator.onRestoreInstanceState(bundle.getParcelable(SAVEDINSTANCESTATE_VIEWANIMATOR));
            }
        }
    }
}


/*
 * Copyright 2014 Niek Haarman
 *
 * 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.nhaarman.listviewanimations;

import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.widget.BaseAdapter;

import com.nhaarman.listviewanimations.util.Insertable;
import com.nhaarman.listviewanimations.util.Swappable;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * A true {@link ArrayList} adapter providing access to some of the {@code ArrayList} methods.
 * <p/>
 * Also implements {@link Swappable} for easy object swapping,
 * and {@link com.nhaarman.listviewanimations.util.Insertable} for inserting objects.
 */
@SuppressWarnings("UnusedDeclaration")
public abstract class ArrayAdapter<T> extends BaseAdapter implements Swappable, Insertable<T> {

    @NonNull
    private final List<T> mItems;

    private BaseAdapter mDataSetChangedSlavedAdapter;

    /**
     * Creates a new ArrayAdapter with an empty {@code List}.
     */
    protected ArrayAdapter() {
        this(null);
    }

    /**
     * Creates a new ArrayAdapter, using (a copy of) given {@code List}, or an empty {@code List} if objects = null.
     */
    protected ArrayAdapter(@Nullable final List<T> objects) {
        if (objects != null) {
            mItems = objects;
        } else {
            mItems = new ArrayList<>();
        }
    }

    @Override
    public int getCount() {
        return mItems.size();
    }

    @Override
    public long getItemId(final int position) {
        return position;
    }

    @Override
    @NonNull
    public T getItem(final int position) {
        return mItems.get(position);
    }

    /**
     * Returns the items.
     */
    @NonNull
    public List<T> getItems() {
        return mItems;
    }

    /**
     * Appends the specified element to the end of the {@code List}.
     *
     * @param object the object to add.
     *
     * @return always true.
     */
    public boolean add(@NonNull final T object) {
        boolean result = mItems.add(object);
        notifyDataSetChanged();
        return result;
    }

    @Override
    public void add(final int index, @NonNull final T item) {
        mItems.add(index, item);
        notifyDataSetChanged();
    }

    /**
     * Adds the objects in the specified collection to the end of this List. The objects are added in the order in which they are returned from the collection's iterator.
     *
     * @param collection the collection of objects.
     *
     * @return {@code true} if this {@code List} is modified, {@code false} otherwise.
     */
    public boolean addAll(@NonNull final Collection<? extends T> collection) {
        boolean result = mItems.addAll(collection);
        notifyDataSetChanged();
        return result;
    }

    public boolean contains(final T object) {
        return mItems.contains(object);
    }

    public void clear() {
        mItems.clear();
        notifyDataSetChanged();
    }

    public boolean remove(@NonNull final Object object) {
        boolean result = mItems.remove(object);
        notifyDataSetChanged();
        return result;
    }

    @NonNull
    public T remove(final int location) {
        T result = mItems.remove(location);
        notifyDataSetChanged();
        return result;
    }

    @Override
    public void swapItems(final int positionOne, final int positionTwo) {
        T firstItem = mItems.set(positionOne, getItem(positionTwo));
        notifyDataSetChanged();
        mItems.set(positionTwo, firstItem);
    }

    public void propagateNotifyDataSetChanged(@NonNull final BaseAdapter slavedAdapter) {
        mDataSetChangedSlavedAdapter = slavedAdapter;
    }

    @Override
    public void notifyDataSetChanged() {
        super.notifyDataSetChanged();
        if (mDataSetChangedSlavedAdapter != null) {
            mDataSetChangedSlavedAdapter.notifyDataSetChanged();
        }
    }
}


package com.nhaarman.listviewanimations.itemmanipulation.dragdrop;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.TargetApi;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewTreeObserver;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.ListAdapter;
import android.widget.WrapperListAdapter;

import com.nhaarman.listviewanimations.itemmanipulation.DynamicListView;
import com.nhaarman.listviewanimations.itemmanipulation.TouchEventHandler;
import com.nhaarman.listviewanimations.util.Swappable;

/**
 * A class which handles drag and drop functionality for listview implementations backed up by a
 * {@link com.nhaarman.listviewanimations.util.Swappable} {@link ListAdapter}.
 * This class only works properly on API levels 14 and higher.
 *

 * Users of this class must call {@link #onTouchEvent(android.view.MotionEvent)} and {@link #dispatchDraw(android.graphics.Canvas)} on the right moments.
 */
@TargetApi(14)
public class DragAndDropHandler implements TouchEventHandler {

    private static final int INVALID_ID = -1;

    @NonNull
    private final DragAndDropListViewWrapper mWrapper;

    /**
     * The {@link ScrollHandler} that handles scrolling when dragging an item.
     */
    @NonNull
    private final ScrollHandler mScrollHandler;

    /**
     * The {@link SwitchViewAnimator} that is responsible for animating the switch views.
     */
    @NonNull
    private final SwitchViewAnimator mSwitchViewAnimator;

    /**
     * The minimum distance in pixels that should be moved before starting vertical item movement.
     */
    private final int mSlop;

    /**
     * The {@link android.widget.ListAdapter} that is assigned. Also implements {@link com.nhaarman.listviewanimations.util.Swappable}.
     */
    @Nullable
    private ListAdapter mAdapter;

    /**
     * The Drawable that is drawn when the user is dragging an item.
     * This value is null if and only if the user is not dragging.
     */
    @Nullable
    private HoverDrawable mHoverDrawable;

    /**
     * The View that is represented by {@link #mHoverDrawable}.
     * When this value is not null, the View should be invisible.
     * This value is null if and only if the user is not dragging.
     */
    @Nullable
    private View mMobileView;

    /**
     * The id of the item view that is being dragged.
     * This value is {@value #INVALID_ID} if and only if the user is not dragging.
     */
    private long mMobileItemId;

    /**
     * The y coordinate of the last non-final {@code MotionEvent}.
     */
    private float mLastMotionEventY = -1;

    /**
     * The original position of the view that is being dragged.
     * This value is {@value android.widget.AdapterView#INVALID_POSITION} if and only if the user is not dragging.
     */
    private int mOriginalMobileItemPosition = AdapterView.INVALID_POSITION;

    /**
     * The {@link DraggableManager} responsible for deciding if an item can be dragged.
     */
    @NonNull
    private DraggableManager mDraggableManager;

    /**
     * The {@link OnItemMovedListener} that is notified of moved items.
     */
    @Nullable
    private OnItemMovedListener mOnItemMovedListener;

    /**
     * The raw x coordinate of the down event.
     */
    private float mDownX;

    /**
     * The raw y coordinate of the down event.
     */
    private float mDownY;

    /**
     * Specifies whether or not the hover drawable is currently being animated as result of an up / cancel event.
     */
    private boolean mIsSettlingHoverDrawable;

    /**
     * Creates a new {@code DragAndDropHandler} for given {@link com.nhaarman.listviewanimations.itemmanipulation.DynamicListView}.
     *
     * @param dynamicListView the {@code DynamicListView} to use.
     */
    public DragAndDropHandler(@NonNull final DynamicListView dynamicListView) {
        this(new DynamicListViewWrapper(dynamicListView));
    }

    /**
     * Creates a new {@code DragAndDropHandler} for the listview implementation
     * in given {@link com.nhaarman.listviewanimations.itemmanipulation.dragdrop.DragAndDropListViewWrapper}
     *
     * @param dragAndDropListViewWrapper the {@code DragAndDropListViewWrapper} which wraps the listview implementation to use.
     */
    public DragAndDropHandler(@NonNull final DragAndDropListViewWrapper dragAndDropListViewWrapper) {
        mWrapper = dragAndDropListViewWrapper;
        if (mWrapper.getAdapter() != null) {
            setAdapterInternal(mWrapper.getAdapter());
        }

        mScrollHandler = new ScrollHandler();
        mWrapper.setOnScrollListener(mScrollHandler);

        mDraggableManager = new DefaultDraggableManager();

        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
            mSwitchViewAnimator = new KitKatSwitchViewAnimator();
        } else {
            mSwitchViewAnimator = new LSwitchViewAnimator();
        }

        mMobileItemId = INVALID_ID;

        ViewConfiguration vc = ViewConfiguration.get(dragAndDropListViewWrapper.getListView().getContext());
        mSlop = vc.getScaledTouchSlop();
    }

    /**
     * @throws java.lang.IllegalStateException    if the adapter does not have stable ids.
     * @throws java.lang.IllegalArgumentException if the adapter does not implement {@link com.nhaarman.listviewanimations.util.Swappable}.
     */
    public void setAdapter(@NonNull final ListAdapter adapter) {
        setAdapterInternal(adapter);
    }

    /**
     * @throws java.lang.IllegalStateException    if the adapter does not have stable ids.
     * @throws java.lang.IllegalArgumentException if the adapter does not implement {@link com.nhaarman.listviewanimations.util.Swappable}.
     */
    private void setAdapterInternal(@NonNull final ListAdapter adapter) {
        ListAdapter actualAdapter = adapter;
        if (actualAdapter instanceof WrapperListAdapter) {
            actualAdapter = ((WrapperListAdapter) actualAdapter).getWrappedAdapter();
        }

        if (!actualAdapter.hasStableIds()) {
            throw new IllegalStateException("Adapter doesn't have stable ids! Make sure your adapter has stable ids, and override hasStableIds() to return true.");
        }

        if (!(actualAdapter instanceof Swappable)) {
            throw new IllegalArgumentException("Adapter should implement Swappable!");
        }

        mAdapter = actualAdapter;
    }

    /**
     * Sets the scroll speed when dragging an item. Defaults to {@code 1.0f}.
     *
     * @param speed {@code <1.0f} to slow down scrolling, {@code >1.0f} to speed up scrolling.
     */
    public void setScrollSpeed(final float speed) {
        mScrollHandler.setScrollSpeed(speed);
    }

    /**
     * Starts dragging the item at given position. User must be touching this {@code DynamicListView}.
     *
     * @param position the position of the item in the adapter to start dragging. Be sure to subtract any header views.
     *
     * @throws java.lang.IllegalStateException if the user is not touching this {@code DynamicListView},
     *                                         or if there is no adapter set.
     */
    public void startDragging(final int position) {
        if (mMobileItemId != INVALID_ID) {
            /* We are already dragging */
            return;
        }

        if (mLastMotionEventY < 0) {
            throw new IllegalStateException("User must be touching the DynamicListView!");
        }

        if (mAdapter == null) {
            throw new IllegalStateException("This DynamicListView has no adapter set!");
        }

        if (position < 0 || position >= mAdapter.getCount()) {
            /* Out of bounds */
            return;
        }

        mMobileView = mWrapper.getChildAt(position - mWrapper.getFirstVisiblePosition() + mWrapper.getHeaderViewsCount());
        if (mMobileView != null) {
            mOriginalMobileItemPosition = position;
            mMobileItemId = mAdapter.getItemId(position);
            mHoverDrawable = new HoverDrawable(mMobileView, mLastMotionEventY);
            mMobileView.setVisibility(View.INVISIBLE);
        }
    }

    /**
     * Sets the {@link DraggableManager} to be used for determining whether an item should be dragged when the user issues a down {@code MotionEvent}.
     */
    public void setDraggableManager(@NonNull final DraggableManager draggableManager) {
        mDraggableManager = draggableManager;
    }

    /**
     * Sets the {@link com.nhaarman.listviewanimations.itemmanipulation.dragdrop.OnItemMovedListener} that is notified when user has dropped a dragging item.
     */
    public void setOnItemMovedListener(@Nullable final OnItemMovedListener onItemMovedListener) {
        mOnItemMovedListener = onItemMovedListener;
    }

    @Override
    public boolean isInteracting() {
        return mMobileItemId != INVALID_ID;
    }

    /**
     * Dispatches the {@link android.view.MotionEvent}s to their proper methods if applicable.
     *
     * @param event the {@code MotionEvent}.
     *
     * @return {@code true} if the event was handled, {@code false} otherwise.
     */
    @Override
    public boolean onTouchEvent(@NonNull final MotionEvent event) {
        boolean handled = false;

        /* We are in the process of animating the hover drawable back, do not start a new drag yet. */
        if (!mIsSettlingHoverDrawable) {
            switch (event.getAction() & MotionEvent.ACTION_MASK) {
                case MotionEvent.ACTION_DOWN:
                    mLastMotionEventY = event.getY();
                    handled = handleDownEvent(event);
                    break;
                case MotionEvent.ACTION_MOVE:
                    mLastMotionEventY = event.getY();
                    handled = handleMoveEvent(event);
                    break;
                case MotionEvent.ACTION_UP:
                    handled = handleUpEvent();
                    mLastMotionEventY = -1;
                    break;
                case MotionEvent.ACTION_CANCEL:
                    handled = handleCancelEvent();
                    mLastMotionEventY = -1;
                    break;
                default:
                    handled = false;
                    break;
            }
        }
        return handled;
    }

    /**
     * Handles the down event.
     *

     * Finds the position and {@code View} of the touch point and, if allowed by the {@link com.nhaarman.listviewanimations.itemmanipulation.dragdrop.DraggableManager},
     * starts dragging the {@code View}.
     *
     * @param event the {@link android.view.MotionEvent} that was triggered.
     *
     * @return {@code true} if we have started dragging, {@code false} otherwise.
     */
    private boolean handleDownEvent(@NonNull final MotionEvent event) {
        mDownX = event.getRawX();
        mDownY = event.getRawY();
        return true;
    }

    /**
     * Retrieves the position in the list corresponding to itemId.
     *
     * @return the position of the item in the list, or {@link android.widget.AdapterView#INVALID_POSITION} if the {@code View} corresponding to the id was not found.
     */
    private int getPositionForId(final long itemId) {
        View v = getViewForId(itemId);
        if (v == null) {
            return AdapterView.INVALID_POSITION;
        } else {
            return mWrapper.getPositionForView(v);
        }
    }

    /**
     * Retrieves the {@code View} in the list corresponding to itemId.
     *
     * @return the {@code View}, or {@code null} if not found.
     */
    @Nullable
    private View getViewForId(final long itemId) {
        ListAdapter adapter = mAdapter;
        if (itemId == INVALID_ID || adapter == null) {
            return null;
        }

        int firstVisiblePosition = mWrapper.getFirstVisiblePosition();

        View result = null;
        for (int i = 0; i < mWrapper.getChildCount() && result == null; i++) {
            int position = firstVisiblePosition + i;
            if (position - mWrapper.getHeaderViewsCount() >= 0) {
                long id = adapter.getItemId(position - mWrapper.getHeaderViewsCount());
                if (id == itemId) {
                    result = mWrapper.getChildAt(i);
                }
            }
        }
        return result;
    }

    /**
     * Handles the move events.
     *

     * Applies the {@link MotionEvent} to the hover drawable, and switches {@code View}s if necessary.
     *
     * @param event the {@code MotionEvent}.
     *
     * @return {@code true} if the event was handled, {@code false} otherwise.
     */
    private boolean handleMoveEvent(@NonNull final MotionEvent event) {
        boolean handled = false;

        float deltaX = event.getRawX() - mDownX;
        float deltaY = event.getRawY() - mDownY;

        if (mHoverDrawable == null && Math.abs(deltaY) > mSlop && Math.abs(deltaY) > Math.abs(deltaX)) {
            int position = mWrapper.pointToPosition((int) event.getX(), (int) event.getY());
            if (position != AdapterView.INVALID_POSITION) {
                View downView = mWrapper.getChildAt(position - mWrapper.getFirstVisiblePosition());
                assert downView != null;
                if (mDraggableManager.isDraggable(downView, position - mWrapper.getHeaderViewsCount(), event.getX() - downView.getX(), event.getY() - downView.getY())) {
                    startDragging(position - mWrapper.getHeaderViewsCount());
                    handled = true;
                }
            }
        } else if (mHoverDrawable != null) {
            mHoverDrawable.handleMoveEvent(event);

            switchIfNecessary();
            mWrapper.getListView().invalidate();
            handled = true;
        }

        return handled;
    }

    /**
     * Finds the {@code View} that is a candidate for switching, and executes the switch if necessary.
     */
    private void switchIfNecessary() {
        if (mHoverDrawable == null || mAdapter == null) {
            return;
        }

        int position = getPositionForId(mMobileItemId);
        long aboveItemId = position - 1 - mWrapper.getHeaderViewsCount() >= 0 ? mAdapter.getItemId(position - 1 - mWrapper.getHeaderViewsCount()) : INVALID_ID;
        long belowItemId = position + 1 - mWrapper.getHeaderViewsCount() < mAdapter.getCount()
                           ? mAdapter.getItemId(position + 1 - mWrapper.getHeaderViewsCount())
                           : INVALID_ID;

        final long switchId = mHoverDrawable.isMovingUpwards() ? aboveItemId : belowItemId;
        View switchView = getViewForId(switchId);

        final int deltaY = mHoverDrawable.getDeltaY();
        if (switchView != null && Math.abs(deltaY) > mHoverDrawable.getIntrinsicHeight()) {
            switchViews(switchView, switchId, mHoverDrawable.getIntrinsicHeight() * (deltaY < 0 ? -1 : 1));
        }

        mScrollHandler.handleMobileCellScroll();

        mWrapper.getListView().invalidate();
    }

    /**
     * Switches the item that is currently being dragged with the item belonging to given id,
     * by notifying the adapter to swap positions and that the data set has changed.
     *
     * @param switchView   the {@code View} that should be animated towards the old position of the currently dragging item.
     * @param switchId     the id of the item that will take the position of the currently dragging item.
     * @param translationY the distance in pixels the {@code switchView} should animate - i.e. the (positive or negative) height of the {@code View} corresponding to the currently
     *                     dragging item.
     */
    private void switchViews(final View switchView, final long switchId, final float translationY) {
        assert mHoverDrawable != null;
        assert mAdapter != null;
        assert mMobileView != null;

        final int switchViewPosition = mWrapper.getPositionForView(switchView);
        int mobileViewPosition = mWrapper.getPositionForView(mMobileView);

        ((Swappable) mAdapter).swapItems(switchViewPosition - mWrapper.getHeaderViewsCount(), mobileViewPosition - mWrapper.getHeaderViewsCount());
        ((BaseAdapter) mAdapter).notifyDataSetChanged();

        mHoverDrawable.shift(switchView.getHeight());
        mSwitchViewAnimator.animateSwitchView(switchId, translationY);
    }

    /**
     * Handles the up event.
     *

     * Animates the hover drawable to its final position, and finalizes our drag properties when the animation has finished.
     * Will also notify the {@link com.nhaarman.listviewanimations.itemmanipulation.dragdrop.OnItemMovedListener} set if applicable.
     *
     * @return {@code true} if the event was handled, {@code false} otherwise.
     */
    private boolean handleUpEvent() {
        if (mMobileView == null) {
            return false;
        }
        assert mHoverDrawable != null;

        ValueAnimator valueAnimator = ValueAnimator.ofInt(mHoverDrawable.getTop(), (int) mMobileView.getY());
        SettleHoverDrawableAnimatorListener listener = new SettleHoverDrawableAnimatorListener(mHoverDrawable, mMobileView);
        valueAnimator.addUpdateListener(listener);
        valueAnimator.addListener(listener);
        valueAnimator.start();

        int newPosition = getPositionForId(mMobileItemId) - mWrapper.getHeaderViewsCount();
        if (mOriginalMobileItemPosition != newPosition && mOnItemMovedListener != null) {
            mOnItemMovedListener.onItemMoved(mOriginalMobileItemPosition, newPosition);
        }

        return true;
    }

    /**
     * Handles the cancel event.
     *
     * @return {@code true} if the event was handled, {@code false} otherwise.
     */
    private boolean handleCancelEvent() {
        return handleUpEvent();
    }

    public void dispatchDraw(@NonNull final Canvas canvas) {
        if (mHoverDrawable != null) {
            mHoverDrawable.draw(canvas);
        }
    }

    /**
     * An interface for animating the switch views.
     * A distinction is made between API levels because {@link android.widget.AbsListView.OnScrollListener#onScroll(android.widget.AbsListView, int, int,
     * int)} calling behavior differs.
     */
    private interface SwitchViewAnimator {

        void animateSwitchView(final long switchId, final float translationY);
    }

    /**
     * By default, nothing is draggable. User should set a {@link com.nhaarman.listviewanimations.itemmanipulation.dragdrop.DraggableManager} manually,
     * or use {@link #startDragging(int)} if they want to start a drag (for example using a long click listener).
     */
    private static class DefaultDraggableManager implements DraggableManager {

        @Override
        public boolean isDraggable(@NonNull final View view, final int position, final float x, final float y) {
            return false;
        }
    }

    /**
     * A {@link SwitchViewAnimator} for versions KitKat and below.
     * This class immediately updates {@link #mMobileView} to be the newly mobile view.
     */
    private class KitKatSwitchViewAnimator implements SwitchViewAnimator {

        @Override
        public void animateSwitchView(final long switchId, final float translationY) {
            assert mMobileView != null;
            mWrapper.getListView().getViewTreeObserver().addOnPreDrawListener(new AnimateSwitchViewOnPreDrawListener(mMobileView, switchId, translationY));
            mMobileView = getViewForId(mMobileItemId);
        }

        private class AnimateSwitchViewOnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {

            private final View mPreviousMobileView;

            private final long mSwitchId;

            private final float mTranslationY;

            AnimateSwitchViewOnPreDrawListener(final View previousMobileView, final long switchId, final float translationY) {
                mPreviousMobileView = previousMobileView;
                mSwitchId = switchId;
                mTranslationY = translationY;
            }

            @Override
            public boolean onPreDraw() {
                mWrapper.getListView().getViewTreeObserver().removeOnPreDrawListener(this);

                View switchView = getViewForId(mSwitchId);
                if (switchView != null) {
                    switchView.setTranslationY(mTranslationY);
                    switchView.animate().translationY(0).start();
                }

                mPreviousMobileView.setVisibility(View.VISIBLE);

                if (mMobileView != null) {
                    mMobileView.setVisibility(View.INVISIBLE);
                }
                return true;
            }
        }
    }

    /**
     * A {@link SwitchViewAnimator} for versions L and above.
     * This class updates {@link #mMobileView} only after the next frame has been drawn.
     */
    private class LSwitchViewAnimator implements SwitchViewAnimator {

        @Override
        public void animateSwitchView(final long switchId, final float translationY) {
            mWrapper.getListView().getViewTreeObserver().addOnPreDrawListener(new AnimateSwitchViewOnPreDrawListener(switchId, translationY));
        }

        private class AnimateSwitchViewOnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {

            private final long mSwitchId;

            private final float mTranslationY;

            AnimateSwitchViewOnPreDrawListener(final long switchId, final float translationY) {
                mSwitchId = switchId;
                mTranslationY = translationY;
            }

            @Override
            public boolean onPreDraw() {
                mWrapper.getListView().getViewTreeObserver().removeOnPreDrawListener(this);

                View switchView = getViewForId(mSwitchId);
                if (switchView != null) {
                    switchView.setTranslationY(mTranslationY);
                    switchView.animate().translationY(0).start();
                }

                assert mMobileView != null;
                mMobileView.setVisibility(View.VISIBLE);
                mMobileView = getViewForId(mMobileItemId);
                assert mMobileView != null;
                mMobileView.setVisibility(View.INVISIBLE);
                return true;
            }
        }
    }

    /**
     * A class which handles scrolling for this {@code DynamicListView} when dragging an item.
     *

     * The {@link #handleMobileCellScroll()} method initiates the scroll and should typically be called on a move {@code MotionEvent}.
     *

     * The {@link #onScroll(android.widget.AbsListView, int, int, int)} method then takes over the functionality {@link #handleMoveEvent(android.view.MotionEvent)} provides.
     */
    private class ScrollHandler implements AbsListView.OnScrollListener {

        private static final int SMOOTH_SCROLL_DP = 3;

        /**
         * The default scroll amount in pixels.
         */
        private final int mSmoothScrollPx;

        /**
         * The factor to multiply {@link #mSmoothScrollPx} with for scrolling.
         */
        private float mScrollSpeedFactor = 1.0f;

        /**
         * The previous first visible item before checking if we should switch.
         */
        private int mPreviousFirstVisibleItem = -1;

        /**
         * The previous last visible item before checking if we should switch.
         */
        private int mPreviousLastVisibleItem = -1;

        /**
         * The current first visible item.
         */
        private int mCurrentFirstVisibleItem;

        /**
         * The current last visible item.
         */
        private int mCurrentLastVisibleItem;

        ScrollHandler() {
            Resources r = mWrapper.getListView().getResources();
            mSmoothScrollPx = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, SMOOTH_SCROLL_DP, r.getDisplayMetrics());
        }

        /**
         * Sets the scroll speed when dragging an item. Defaults to {@code 1.0f}.
         *
         * @param scrollSpeedFactor {@code <1.0f} to slow down scrolling, {@code >1.0f} to speed up scrolling.
         */
        void setScrollSpeed(final float scrollSpeedFactor) {
            mScrollSpeedFactor = scrollSpeedFactor;
        }

        /**
         * Scrolls the {@code DynamicListView} if the hover drawable is above or below the bounds of the {@code ListView}.
         */
        void handleMobileCellScroll() {
            if (mHoverDrawable == null || mIsSettlingHoverDrawable) {
                return;
            }

            Rect r = mHoverDrawable.getBounds();
            int offset = mWrapper.computeVerticalScrollOffset();
            int height = mWrapper.getListView().getHeight();
            int extent = mWrapper.computeVerticalScrollExtent();
            int range = mWrapper.computeVerticalScrollRange();
            int hoverViewTop = r.top;
            int hoverHeight = r.height();

            int scrollPx = (int) Math.max(1, mSmoothScrollPx * mScrollSpeedFactor);
            if (hoverViewTop <= 0 && offset > 0) {
                mWrapper.smoothScrollBy(-scrollPx, 0);
            } else if (hoverViewTop + hoverHeight >= height && offset + extent < range) {
                mWrapper.smoothScrollBy(scrollPx, 0);
            }
        }

        @Override
        public void onScroll(@NonNull final AbsListView view, final int firstVisibleItem, final int visibleItemCount, final int totalItemCount) {
            mCurrentFirstVisibleItem = firstVisibleItem;
            mCurrentLastVisibleItem = firstVisibleItem + visibleItemCount;

            mPreviousFirstVisibleItem = mPreviousFirstVisibleItem == -1 ? mCurrentFirstVisibleItem : mPreviousFirstVisibleItem;
            mPreviousLastVisibleItem = mPreviousLastVisibleItem == -1 ? mCurrentLastVisibleItem : mPreviousLastVisibleItem;

            if (mHoverDrawable != null) {
                assert mMobileView != null;
                float y = mMobileView.getY();
                mHoverDrawable.onScroll(y);
            }

            if (!mIsSettlingHoverDrawable) {
                checkAndHandleFirstVisibleCellChange();
                checkAndHandleLastVisibleCellChange();
            }

            mPreviousFirstVisibleItem = mCurrentFirstVisibleItem;
            mPreviousLastVisibleItem = mCurrentLastVisibleItem;
        }

        @Override
        public void onScrollStateChanged(@NonNull final AbsListView view, final int scrollState) {
            if (scrollState == SCROLL_STATE_IDLE && mHoverDrawable != null) {
                handleMobileCellScroll();
            }
        }

        /**
         * Determines if the listview scrolled up enough to reveal a new cell at the
         * top of the list. If so, switches the newly shown view with the mobile view.
         */
        private void checkAndHandleFirstVisibleCellChange() {
            if (mHoverDrawable == null || mAdapter == null || mCurrentFirstVisibleItem >= mPreviousFirstVisibleItem) {
                return;
            }

            int position = getPositionForId(mMobileItemId);
            if (position == AdapterView.INVALID_POSITION) {
                return;
            }

            long switchItemId = position - 1 - mWrapper.getHeaderViewsCount() >= 0 ? mAdapter.getItemId(position - 1 - mWrapper.getHeaderViewsCount()) : INVALID_ID;
            View switchView = getViewForId(switchItemId);
            if (switchView != null) {
                switchViews(switchView, switchItemId, -switchView.getHeight());
            }
        }

        /**
         * Determines if the listview scrolled down enough to reveal a new cell at the
         * bottom of the list. If so, switches the newly shown view with the mobile view.
         */
        private void checkAndHandleLastVisibleCellChange() {
            if (mHoverDrawable == null || mAdapter == null || mCurrentLastVisibleItem <= mPreviousLastVisibleItem) {
                return;
            }

            int position = getPositionForId(mMobileItemId);
            if (position == AdapterView.INVALID_POSITION) {
                return;
            }

            long switchItemId = position + 1 - mWrapper.getHeaderViewsCount() < mAdapter.getCount()
                                ? mAdapter.getItemId(position + 1 - mWrapper.getHeaderViewsCount())
                                : INVALID_ID;
            View switchView = getViewForId(switchItemId);
            if (switchView != null) {
                switchViews(switchView, switchItemId, switchView.getHeight());
            }
        }
    }

    /**
     * Updates the hover drawable's bounds with the animated values.
     * When the animation has finished, it will reset all the drag properties.
     */
    private class SettleHoverDrawableAnimatorListener extends AnimatorListenerAdapter implements ValueAnimator.AnimatorUpdateListener {

        @NonNull
        private final HoverDrawable mAnimatingHoverDrawable;

        @NonNull
        private final View mAnimatingMobileView;

        private SettleHoverDrawableAnimatorListener(@NonNull final HoverDrawable animatingHoverDrawable, @NonNull final View animatingMobileView) {
            mAnimatingHoverDrawable = animatingHoverDrawable;
            mAnimatingMobileView = animatingMobileView;
        }

        @Override
        public void onAnimationStart(final Animator animation) {
            mIsSettlingHoverDrawable = true;
        }

        @Override
        public void onAnimationUpdate(final ValueAnimator animation) {
            mAnimatingHoverDrawable.setTop((Integer) animation.getAnimatedValue());
            mWrapper.getListView().postInvalidate();
        }

        @Override
        public void onAnimationEnd(final Animator animation) {
            mAnimatingMobileView.setVisibility(View.VISIBLE);

            mHoverDrawable = null;
            mMobileView = null;
            mMobileItemId = INVALID_ID;
            mOriginalMobileItemPosition = AdapterView.INVALID_POSITION;

            mIsSettlingHoverDrawable = false;
        }
    }
}


/*
 * Copyright 2014 Niek Haarman
 *
 * 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.nhaarman.listviewanimations.itemmanipulation.animateaddition;

import android.support.annotation.NonNull;
import android.util.Pair;

import com.nhaarman.listviewanimations.util.Insertable;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * A class to insert items only when there are no active items.
 * A pending index-item pair can have two states: active and pending. When inserting new items, the {@link com.nhaarman.listviewanimations.itemmanipulation
 * .AnimateAdditionAdapter.Insertable#add
 * (int, Object)} method will be called directly if there are no active index-item pairs.
 * Otherwise, pairs will be queued until the active list is empty.
 */
public class InsertQueue<T> {

    @NonNull
    private final Insertable<T> mInsertable;

    @NonNull
    private final Collection<AtomicInteger> mActiveIndexes = new HashSet<>();

    @NonNull
    private final List<Pair<Integer, T>> mPendingItemsToInsert = new ArrayList<>();

    public InsertQueue(@NonNull final Insertable<T> insertable) {
        mInsertable = insertable;
    }

    /**
     * Insert an item into the queue at given index. Will directly call {@link Insertable#add(int,
     * Object)} if there are no active index-item pairs. Otherwise, the pair will be queued.
     *
     * @param index the index at which the item should be inserted.
     * @param item  the item to insert.
     */
    public void insert(final int index, @NonNull final T item) {
        if (mActiveIndexes.isEmpty() && mPendingItemsToInsert.isEmpty()) {
            mActiveIndexes.add(new AtomicInteger(index));
            //noinspection unchecked
            mInsertable.add(index, item);
        } else {
            mPendingItemsToInsert.add(new Pair<>(index, item));
        }
    }

    public void insert(@NonNull final Pair<Integer, T>... indexItemPair) {
        insert(Arrays.asList(indexItemPair));
    }

    public void insert(@NonNull final Collection<Pair<Integer, T>> indexItemPairs) {
        if (mActiveIndexes.isEmpty() && mPendingItemsToInsert.isEmpty()) {
            for (Pair<Integer, T> pair : indexItemPairs) {
                for (AtomicInteger existing : mActiveIndexes) {
                    if (existing.intValue() >= pair.first) {
                        existing.incrementAndGet();
                    }
                }
                mActiveIndexes.add(new AtomicInteger(pair.first));

                mInsertable.add(pair.first, pair.second);
            }
        } else {
            mPendingItemsToInsert.addAll(indexItemPairs);
        }
    }

    /**
     * Clears the active states and inserts any pending pairs if applicable.
     */
    public void clearActive() {
        mActiveIndexes.clear();
        insertPending();
    }

    /**
     * Clear the active state for given index. Will insert any pending pairs if this call leads to a state where there are no active pairs.
     *
     * @param index the index to remove.
     */
    public void removeActiveIndex(final int index) {
        boolean found = false;
        for (Iterator<AtomicInteger> iterator = mActiveIndexes.iterator(); iterator.hasNext() && !found; ) {
            if (iterator.next().get() == index) {
                iterator.remove();
                found = true;
            }
        }
        if (mActiveIndexes.isEmpty()) {
            insertPending();
        }
    }

    /**
     * Inserts pending items into the Insertable, and adds them to the active positions (correctly managing position shifting). Clears the pending items.
     */
    private void insertPending() {
        for (Pair<Integer, T> pi : mPendingItemsToInsert) {
            for (AtomicInteger existing : mActiveIndexes) {
                if (existing.intValue() >= pi.first) {
                    existing.incrementAndGet();
                }
            }
            mActiveIndexes.add(new AtomicInteger(pi.first));
            mInsertable.add(pi.first, pi.second);
        }
        mPendingItemsToInsert.clear();
    }

    /**
     * Returns a collection of currently active indexes.
     */
    @NonNull
    public Collection<Integer> getActiveIndexes() {
        Collection<Integer> result = new HashSet<>();
        for (AtomicInteger i : mActiveIndexes) {
            result.add(i.get());
        }
        return result;
    }

    /**
     * Returns a {@code List} of {@code Pair}s with the index and items that are pending to be inserted, in the order they were requested.
     */
    @NonNull
    public List<Pair<Integer, T>> getPendingItemsToInsert() {
        return mPendingItemsToInsert;
    }
}