package com.github.davidmoten.rx;

import java.io.File;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchEvent.Kind;
import java.nio.file.WatchService;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

import com.github.davidmoten.guavamini.Optional;
import com.github.davidmoten.guavamini.Preconditions;
import com.github.davidmoten.rx.internal.operators.OnSubscribeWatchServiceEvents;
import com.github.davidmoten.rx.internal.operators.OperatorFileTailer;
import com.github.davidmoten.rx.util.BackpressureStrategy;

import rx.Observable;
import rx.Scheduler;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.functions.Func0;
import rx.functions.Func1;
import rx.observables.GroupedObservable;

/**
 * Observable utility methods related to {@link File}.
 */
public final class FileObservable {

    public static final int DEFAULT_MAX_BYTES_PER_EMISSION = 8192;

    private FileObservable() {
        // prevent instantiation
    }

    /**
     * Returns an {@link Observable} that uses NIO {@link WatchService} (and a
     * dedicated thread) to push modified events to an observable that reads and
     * reports new sequences of bytes to a subscriber. The NIO
     * {@link WatchService} events are sampled according to
     * sampleTimeMs so that lots of discrete activity on a file
     * (for example a log file with very frequent entries) does not prompt an
     * inordinate number of file reads to pick up changes.
     *
     * @param file
     *            the file to tail
     * @param startPosition
     *            start tailing file at position in bytes
     * @param sampleTimeMs
     *            sample time in millis
     * @param chunkSize
     *            max array size of each element emitted by the Observable. Is
     *            also used as the buffer size for reading from the file. Try
     *            {@link FileObservable#DEFAULT_MAX_BYTES_PER_EMISSION} if you
     *            don't know what to put here.
     * @return observable of byte arrays
     */
    public final static Observable<byte[]> tailFile(File file, long startPosition,
            long sampleTimeMs, int chunkSize) {
        Preconditions.checkNotNull(file);
        Observable<Object> events = from(file, StandardWatchEventKinds.ENTRY_CREATE,
                StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.OVERFLOW)
                        // don't care about the event details, just that there
                        // is one
                        .cast(Object.class)
                        // get lines once on subscription so we tail the lines
                        // in the file at startup
                        .startWith(new Object());
        return tailFile(file, startPosition, sampleTimeMs, chunkSize, events);
    }

    /**
     * Returns an {@link Observable} that uses given given observable to push
     * modified events to an observable that reads and reports new sequences of
     * bytes to a subscriber. The NIO {@link WatchService} MODIFY and OVERFLOW
     * events are sampled according to sampleTimeMs so that lots of
     * discrete activity on a file (for example a log file with very frequent
     * entries) does not prompt an inordinate number of file reads to pick up
     * changes. File create events are not sampled and are always passed
     * through.
     *
     * @param file
     *            the file to tail
     * @param startPosition
     *            start tailing file at position in bytes
     * @param sampleTimeMs
     *            sample time in millis for MODIFY and OVERFLOW events
     * @param chunkSize
     *            max array size of each element emitted by the Observable. Is
     *            also used as the buffer size for reading from the file. Try
     *            {@link FileObservable#DEFAULT_MAX_BYTES_PER_EMISSION} if you
     *            don't know what to put here.
     * @param events
     *            trigger a check for file changes. Use
     *            {@link Observable#interval(long, TimeUnit)} for example.
     * @return observable of byte arrays
     */
    public final static Observable<byte[]> tailFile(File file, long startPosition,
            long sampleTimeMs, int chunkSize, Observable<?> events) {
        Preconditions.checkNotNull(file);
        return sampleModifyOrOverflowEventsOnly(events, sampleTimeMs)
                // tail file triggered by events
                .lift(new OperatorFileTailer(file, startPosition, chunkSize));
    }

    /**
     * Returns an {@link Observable} that uses NIO {@link WatchService} (and a
     * dedicated thread) to push modified events to an observable that reads and
     * reports new lines to a subscriber. The NIO WatchService MODIFY and
     * OVERFLOW events are sampled according to sampleTimeMs so
     * that lots of discrete activity on a file (for example a log file with
     * very frequent entries) does not prompt an inordinate number of file reads
     * to pick up changes. File create events are not sampled and are always
     * passed through.
     *
     * @param file
     *            the file to tail
     * @param startPosition
     *            start tailing file at position in bytes
     * @param sampleTimeMs
     *            sample time in millis for MODIFY and OVERFLOW events
     * @param charset
     *            the character set to use to decode the bytes to a string
     * @return observable of strings
     */
    public final static Observable<String> tailTextFile(File file, long startPosition,
            long sampleTimeMs, Charset charset) {
        return toLines(tailFile(file, startPosition, sampleTimeMs, DEFAULT_MAX_BYTES_PER_EMISSION),
                charset);
    }

    /**
     * Returns an {@link Observable} of String that uses the given events stream
     * to trigger checks on file change so that new lines can be read and
     * emitted.
     *
     * @param file
     *            the file to tail, cannot be null
     * @param startPosition
     *            start tailing file at position in bytes
     * @param chunkSize
     *            max array size of each element emitted by the Observable. Is
     *            also used as the buffer size for reading from the file. Try
     *            {@link FileObservable#DEFAULT_MAX_BYTES_PER_EMISSION} if you
     *            don't know what to put here.
     * @param charset
     *            the character set to use to decode the bytes to a string
     * @param events
     *            trigger a check for file changes. Use
     *            {@link Observable#interval(long, TimeUnit)} for example.
     * @return observable of strings
     */
    public final static Observable<String> tailTextFile(File file, long startPosition,
            int chunkSize, Charset charset, Observable<?> events) {
        Preconditions.checkNotNull(file);
        Preconditions.checkNotNull(charset);
        Preconditions.checkNotNull(events);
        return toLines(events.lift(new OperatorFileTailer(file, startPosition, chunkSize))
                .onBackpressureBuffer(), charset);
    }

    /**
     * Returns an {@link Observable} of {@link WatchEvent}s from a
     * {@link WatchService}.
     *
     * @param watchService
     *            WatchService to generate events from
     * @param scheduler
     *            schedules polls of the watchService
     * @param pollDuration
     *            duration of each poll
     * @param pollDurationUnit
     *            time unit for the duration of each poll
     * @param pollInterval
     *            interval between polls of the watchService
     * @param pollIntervalUnit
     *            time unit for the interval between polls
     * @param backpressureStrategy
     *            backpressures strategy to apply
     * @return an observable of WatchEvents from watchService
     */
    public final static Observable<WatchEvent<?>> from(WatchService watchService,
            Scheduler scheduler, long pollDuration, TimeUnit pollDurationUnit, long pollInterval,
            TimeUnit pollIntervalUnit, BackpressureStrategy backpressureStrategy) {
        Preconditions.checkNotNull(watchService);
        Preconditions.checkNotNull(scheduler);
        Preconditions.checkNotNull(pollDurationUnit);
        Preconditions.checkNotNull(backpressureStrategy);
        Observable<WatchEvent<?>> o = Observable
                .create(new OnSubscribeWatchServiceEvents(watchService, scheduler, pollDuration,
                        pollDurationUnit, pollInterval, pollIntervalUnit));
        if (backpressureStrategy == BackpressureStrategy.BUFFER) {
            return o.onBackpressureBuffer();
        } else if (backpressureStrategy == BackpressureStrategy.DROP)
            return o.onBackpressureDrop();
        else if (backpressureStrategy == BackpressureStrategy.LATEST)
            return o.onBackpressureLatest();
        else
            throw new RuntimeException("unrecognized backpressureStrategy " + backpressureStrategy);
    }

    /**
     * Returns an {@link Observable} of {@link WatchEvent}s from a
     * {@link WatchService}.
     *
     * @param watchService
     *            {@link WatchService} to generate events for
     * @return observable of watch events from the watch service
     */
    public final static Observable<WatchEvent<?>> from(WatchService watchService) {
        return from(watchService, rx.schedulers.Schedulers.trampoline(), Long.MAX_VALUE,
                TimeUnit.MILLISECONDS, 0, TimeUnit.SECONDS, BackpressureStrategy.BUFFER);
    }

    /**
     * If file does not exist at subscribe time then is assumed to not be a
     * directory. If the file is not a directory (bearing in mind the aforesaid
     * assumption) then a {@link WatchService} is set up on its parent and
     * {@link WatchEvent}s of the given kinds are filtered to concern the file
     * in question. If the file is a directory then a {@link WatchService} is
     * set up on the directory and all events are passed through of the given
     * kinds.
     *
     * @param file
     *            file to watch
     * @param kinds
     *            event kinds to watch for and emit
     * @return observable of watch events
     */
    @SafeVarargs
    public final static Observable<WatchEvent<?>> from(final File file, Kind<?>... kinds) {
        return from(file, null, kinds);
    }

    /**
     * If file does not exist at subscribe time then is assumed to not be a
     * directory. If the file is not a directory (bearing in mind the aforesaid
     * assumption) then a {@link WatchService} is set up on its parent and
     * {@link WatchEvent}s of the given kinds are filtered to concern the file
     * in question. If the file is a directory then a {@link WatchService} is
     * set up on the directory and all events are passed through of the given
     * kinds.
     *
     * @param file
     *            file to watch
     * @param kinds
     *            event kinds to watch for and emit
     * @return observable of watch events
     */
    public final static Observable<WatchEvent<?>> from(final File file, List<Kind<?>> kinds) {
        return from(file, null, kinds.toArray(new Kind<?>[] {}));
    }

    /**
     * If file does not exist at subscribe time then is assumed to not be a
     * directory. If the file is not a directory (bearing in mind the aforesaid
     * assumption) then a {@link WatchService} is set up on its parent and
     * {@link WatchEvent}s of the given kinds are filtered to concern the file
     * in question. If the file is a directory then a {@link WatchService} is
     * set up on the directory and all events are passed through of the given
     * kinds.
     *
     * @param file
     *            file to generate watch events from
     * @param onWatchStarted
     *            called when WatchService is created
     * @param kinds
     *            kinds of watch events to register for
     * @return observable of watch events
     */
    public final static Observable<WatchEvent<?>> from(final File file,
            final Action0 onWatchStarted, Kind<?>... kinds) {
        return watchService(file, kinds)
                // when watch service created call onWatchStarted
                .doOnNext(new Action1<WatchService>() {
                    @Override
                    public void call(WatchService w) {
                        if (onWatchStarted != null)
                            onWatchStarted.call();
                    }
                })
                // emit events from the WatchService
                .flatMap(TO_WATCH_EVENTS)
                // restrict to events related to the file
                .filter(onlyRelatedTo(file));
    }

    /**
     * Creates a {@link WatchService} on subscribe for the given file and event
     * kinds.
     *
     * @param file
     *            the file to watch
     * @param kinds
     *            event kinds to watch for
     * @return observable of watch events
     */
    @SafeVarargs
    public final static Observable<WatchService> watchService(final File file,
            final Kind<?>... kinds) {
        return Observable.defer(new Func0<Observable<WatchService>>() {

            @Override
            public Observable<WatchService> call() {
                try {
                    final Path path = getBasePath(file);
                    WatchService watchService = path.getFileSystem().newWatchService();
                    path.register(watchService, kinds);
                    return Observable.just(watchService);
                } catch (Exception e) {
                    return Observable.error(e);
                }
            }
        });

    }

    private final static Path getBasePath(final File file) {
        final Path path;
        if (file.exists() && file.isDirectory())
            path = Paths.get(file.toURI());
        else
            path = Paths.get(file.getParentFile().toURI());
        return path;
    }

    /**
     * Returns true if and only if the path corresponding to a WatchEvent
     * represents the given file. This will be the case for Create, Modify,
     * Delete events.
     *
     * @param file
     *            the file to restrict events to
     * @return predicate
     */
    private final static Func1<WatchEvent<?>, Boolean> onlyRelatedTo(final File file) {
        return new Func1<WatchEvent<?>, Boolean>() {

            @Override
            public Boolean call(WatchEvent<?> event) {

                final boolean ok;
                if (file.isDirectory())
                    ok = true;
                else if (StandardWatchEventKinds.OVERFLOW.equals(event.kind()))
                    ok = true;
                else {
                    Object context = event.context();
                    if (context != null && context instanceof Path) {
                        Path p = (Path) context;
                        Path basePath = getBasePath(file);
                        File pFile = new File(basePath.toFile(), p.toString());
                        ok = pFile.getAbsolutePath().equals(file.getAbsolutePath());
                    } else
                        ok = false;
                }
                return ok;
            }
        };
    }

    private static Observable<String> toLines(Observable<byte[]> bytes, Charset charset) {
        return Strings.split(Strings.decode(bytes, charset), "\n");
    }

    private final static Func1<WatchService, Observable<WatchEvent<?>>> TO_WATCH_EVENTS = new Func1<WatchService, Observable<WatchEvent<?>>>() {

        @Override
        public Observable<WatchEvent<?>> call(WatchService watchService) {
            return from(watchService);
        }
    };

    private static Observable<Object> sampleModifyOrOverflowEventsOnly(Observable<?> events,
            final long sampleTimeMs) {
        return events
                // group by true if is modify or overflow, false otherwise
                .groupBy(IS_MODIFY_OR_OVERFLOW)
                // only sample if is modify or overflow
                .flatMap(sampleIfTrue(sampleTimeMs));
    }

    private static Func1<GroupedObservable<Boolean, ?>, Observable<?>> sampleIfTrue(
            final long sampleTimeMs) {
        return new Func1<GroupedObservable<Boolean, ?>, Observable<?>>() {

            @Override
            public Observable<?> call(GroupedObservable<Boolean, ?> group) {
                // if is modify or overflow WatchEvent
                if (group.getKey())
                    return group.sample(sampleTimeMs, TimeUnit.MILLISECONDS);
                else
                    return group;
            }
        };
    }

    private static Func1<Object, Boolean> IS_MODIFY_OR_OVERFLOW = new Func1<Object, Boolean>() {

        @Override
        public Boolean call(Object event) {
            if (event instanceof WatchEvent) {
                WatchEvent<?> w = (WatchEvent<?>) event;
                String kind = w.kind().name();
                if (kind.equals(StandardWatchEventKinds.ENTRY_MODIFY.name())
                        || kind.equals(StandardWatchEventKinds.OVERFLOW.name())) {
                    return true;
                } else
                    return false;
            } else
                return false;
        }
    };

    public static WatchEventsBuilder from(File file) {
        return new WatchEventsBuilder(file);
    }

    public static final class WatchEventsBuilder {
        private final File file;
        private Optional<Scheduler> scheduler = Optional.absent();
        private long pollInterval = 0;
        private TimeUnit pollIntervalUnit = TimeUnit.MILLISECONDS;
        private Optional<Long> pollDuration = Optional.absent();
        private TimeUnit pollDurationUnit = TimeUnit.MILLISECONDS;
        private final List<Kind<?>> kinds = new ArrayList<>();
        private BackpressureStrategy backpressureStrategy = BackpressureStrategy.BUFFER;

        private WatchEventsBuilder(File file) {
            this.file = file;
        }

        public WatchEventsBuilder scheduler(Scheduler scheduler) {
            this.scheduler = Optional.of(scheduler);
            return this;
        }

        public WatchEventsBuilder pollInterval(long interval, TimeUnit unit) {
            this.pollInterval = interval;
            this.pollIntervalUnit = unit;
            if (!pollDuration.isPresent())
                this.pollDuration = Optional.of(0L);
            return this;
        }

        public WatchEventsBuilder pollDuration(long duration, TimeUnit unit) {
            this.pollDuration = Optional.of(duration);
            this.pollDurationUnit = unit;
            return this;
        }

        public WatchEventsBuilder kind(Kind<?> kind) {
            this.kinds.add(kind);
            return this;
        }

        public WatchEventsBuilder kinds(Kind<?>... kinds) {
            for (Kind<?> kind : kinds) {
                this.kinds.add(kind);
            }
            return this;
        }

        public WatchEventsBuilder onBackpressure(BackpressureStrategy strategy) {
            this.backpressureStrategy = strategy;
            return this;
        }

        public Observable<WatchEvent<?>> events() {
            return watchService(file, kinds.toArray(new Kind<?>[] {}))
                    .flatMap(new Func1<WatchService, Observable<WatchEvent<?>>>() {
                        @Override
                        public Observable<WatchEvent<?>> call(WatchService watchService) {
                            if (!scheduler.isPresent()) {
                                if (!pollDuration.isPresent() || pollDuration.get() == 0) {
                                    scheduler = Optional.of(rx.schedulers.Schedulers.computation());
                                } else {
                                    // poll will block so don't do on
                                    // computation()
                                    scheduler = Optional.of(rx.schedulers.Schedulers.io());
                                }
                            }
                            return from(watchService, scheduler.get(),
                                    pollDuration.or(Long.MAX_VALUE), pollDurationUnit, pollInterval,
                                    pollIntervalUnit, backpressureStrategy);
                        }
                    });
        }

    }

    public static TailerBuilder tailer() {
        return new TailerBuilder();
    }

    public static final class TailerBuilder {

        private File file = null;
        private long startPosition = 0;
        private long sampleTimeMs = 500;
        private int chunkSize = 8192;
        private Charset charset = Charset.defaultCharset();
        private Observable<?> source = null;
        private Action0 onWatchStarted = new Action0() {
            @Override
            public void call() {
                // do nothing
            }
        };

        private TailerBuilder() {
        }

        /**
         * The file to tail.
         *
         * @param file
         *            file to tail
         * @return the builder (this)
         */
        public TailerBuilder file(File file) {
            this.file = file;
            return this;
        }

        public TailerBuilder file(String filename) {
            return file(new File(filename));
        }

        public TailerBuilder onWatchStarted(Action0 onWatchStarted) {
            this.onWatchStarted = onWatchStarted;
            return this;
        }

        /**
         * The startPosition in bytes in the file to commence the tail from. 0 =
         * start of file. Defaults to 0.
         *
         * @param startPosition
         *            start position
         * @return this
         */
        public TailerBuilder startPosition(long startPosition) {
            this.startPosition = startPosition;
            return this;
        }

        /**
         * Specifies sampling to apply to the source observable (which could be
         * very busy if a lot of writes are occurring for example). Sampling is
         * only applied to file updates (MODIFY and OVERFLOW), file creation
         * events are always passed through. File deletion events are ignored
         * (in fact are not requested of NIO).
         *
         * @param sampleTimeMs
         *            sample time in ms
         * @return this
         */
        public TailerBuilder sampleTimeMs(long sampleTimeMs) {
            this.sampleTimeMs = sampleTimeMs;
            return this;
        }

        /**
         * Emissions from the tailed file will be no bigger than this.
         *
         * @param chunkSize
         *            chunk size in bytes
         * @return this
         */
        public TailerBuilder chunkSize(int chunkSize) {
            this.chunkSize = chunkSize;
            return this;
        }

        /**
         * The charset of the file. Only used for tailing a text file.
         *
         * @param charset
         *            charset to decode with
         * @return this
         */
        public TailerBuilder charset(Charset charset) {
            this.charset = charset;
            return this;
        }

        /**
         * The charset of the file. Only used for tailing a text file.
         *
         * @param charset
         *            charset to decode the file with
         * @return this
         */
        public TailerBuilder charset(String charset) {
            return charset(Charset.forName(charset));
        }

        public TailerBuilder utf8() {
            return charset("UTF-8");
        }

        public TailerBuilder source(Observable<?> source) {
            this.source = source;
            return this;
        }

        public Observable<byte[]> tail() {

            return tailFile(file, startPosition, sampleTimeMs, chunkSize, getSource());
        }

        public Observable<String> tailText() {
            return tailTextFile(file, startPosition, chunkSize, charset, getSource());
        }

        private Observable<?> getSource() {
            if (source == null)
                return from(file, onWatchStarted, StandardWatchEventKinds.ENTRY_CREATE,
                        StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.OVERFLOW);
            else
                return source;

        }

    }

}


package com.github.davidmoten.rx.internal.operators;

import java.io.IOException;
import java.nio.file.ClosedWatchServiceException;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.concurrent.TimeUnit;

import rx.Observable.OnSubscribe;
import rx.Scheduler;
import rx.Scheduler.Worker;
import rx.Subscriber;
import rx.Subscription;
import rx.functions.Action0;
import rx.subscriptions.Subscriptions;

public final class OnSubscribeWatchServiceEvents implements OnSubscribe<WatchEvent<?>> {

    private final Scheduler scheduler;
    private final WatchService watchService;
    private final long pollDurationMs;
    private final long pollIntervalMs;

    public OnSubscribeWatchServiceEvents(WatchService watchService, Scheduler scheduler,
            long pollDuration, TimeUnit pollDurationUnit, long pollInterval,
            TimeUnit pollIntervalUnit) {
        this.watchService = watchService;
        this.scheduler = scheduler;
        this.pollDurationMs = pollDurationUnit.toMillis(pollDuration);
        this.pollIntervalMs = pollIntervalUnit.toMillis(pollInterval);
    }

    @Override
    public void call(final Subscriber<? super WatchEvent<?>> subscriber) {
        final Worker worker = scheduler.createWorker();
        subscriber.add(worker);
        subscriber.add(createSubscriptionToCloseWatchService(watchService));
        worker.schedule(new Action0() {
            @Override
            public void call() {
                if (emitEvents(watchService, subscriber, pollDurationMs, pollIntervalMs)) {
                    worker.schedule(this);
                }
            }
        }, pollIntervalMs, TimeUnit.MILLISECONDS);
    }

    // returns true if and only there may be more events
    private static boolean emitEvents(WatchService watchService,
            Subscriber<? super WatchEvent<?>> subscriber, long pollDurationMs,
            long pollIntervalMs) {
        // get the first event
        WatchKey key = nextKey(watchService, subscriber, pollDurationMs);

        if (key != null) {
            if (subscriber.isUnsubscribed())
                return false;
            // we have a polled event, now we traverse it and
            // receive all the states from it
            for (WatchEvent<?> event : key.pollEvents()) {
                if (subscriber.isUnsubscribed())
                    return false;
                else
                    subscriber.onNext(event);
            }

            boolean valid = key.reset();
            if (!valid && !subscriber.isUnsubscribed()) {
                subscriber.onCompleted();
                return false;
            } else if (!valid)
                return false;
        }
        return true;
    }

    private static WatchKey nextKey(WatchService watchService,
            Subscriber<? super WatchEvent<?>> subscriber, long pollDurationMs) {
        try {
            // this command blocks but unsubscribe closes the watch
            // service and interrupts it
            if (pollDurationMs == 0) {
                return watchService.poll();
            } else if (pollDurationMs == Long.MAX_VALUE) {
                // blocking
                return watchService.take();
            } else {
                // blocking
                return watchService.poll(pollDurationMs, TimeUnit.MILLISECONDS);
            }
        } catch (ClosedWatchServiceException e) {
            // must have unsubscribed
            if (!subscriber.isUnsubscribed())
                subscriber.onCompleted();
            return null;
        } catch (InterruptedException e) {
            // this case is problematic because unsubscribe may call
            // Thread.interrupt() before calling the unsubscribe method of
            // the Subscription. Thus at this point we don't know if a
            // deliberate interrupt was called in which case I would call
            // onComplete or if unsubscribe was called in which case I
            // should not call anything. For the moment I choose to not call
            // anything partly because a deliberate stop of the
            // watchService.take ignorant of the Observable should ideally
            // happen via a call to the WatchService.close() method rather
            // than Thread.interrupt().
            // TODO raise the issue with RxJava team in particular
            // Subscriptions.from(Future) calling FutureTask.cancel(true)
            try {
                watchService.close();
            } catch (IOException e1) {
                // do nothing
            }
            return null;
        }
    }

    private final static Subscription createSubscriptionToCloseWatchService(
            final WatchService watchService) {
        return Subscriptions.create(new Action0() {

            @Override
            public void call() {
                try {
                    watchService.close();
                } catch (ClosedWatchServiceException e) {
                    // do nothing
                } catch (IOException e) {
                    // do nothing
                }
            }
        });
    }
}


package it.xabaras.android.viewpagerindicator.widget;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.StateListDrawable;
import android.graphics.drawable.shapes.OvalShape;
import android.support.v4.content.ContextCompat;
import android.support.v4.view.ViewPager;
import android.support.v7.widget.AppCompatRadioButton;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.widget.RadioGroup;

import it.xabaras.android.viewpagerindicator.R;

/**
 * Created by Paolo Montalto on 19/07/16.
 */

/**
 * This is simple page indicator for android ViewPager.
 */
public class ViewPagerIndicator extends RadioGroup {
    private static final String TAG = "ViewPagerIndicator";
    private ViewPager mViewPager;
    private int mItemDividerWidth;
    private Drawable mButtonDrawable;
    private int mItemRadius;
    private int mItemSelectedColor;
    private int mItemUnselectedColor;

    /**
     * Default Constructor
     * @param context a Context
     */
    public ViewPagerIndicator(Context context) {
        this(context, null);
    }

    /**
     * Constructor with attributes
     * @param context a Context
     * @param attrs an Attribute set
     */
    public ViewPagerIndicator(Context context, AttributeSet attrs) {
        super(context, attrs);

        try {
            setOrientation(HORIZONTAL);
            setGravity(Gravity.CENTER);

            TypedArray a = context.getTheme().obtainStyledAttributes(
                    attrs,
                    R.styleable.ViewPagerIndicator,
                    0, 0);

            mItemRadius = getResources().getDimensionPixelSize(R.dimen.default_item_radius);
            mItemRadius = a.getDimensionPixelSize(R.styleable.ViewPagerIndicator_itemRadius, mItemRadius);
            mItemDividerWidth = a.getDimensionPixelSize(R.styleable.ViewPagerIndicator_itemDividerWidth, getResources().getDimensionPixelSize(R.dimen.default_item_divider_width));
            int theme = a.getInt(R.styleable.ViewPagerIndicator_defaultIndicatorTheme, 0);
            if ( theme == 0 ) {
                mItemSelectedColor = ContextCompat.getColor(getContext(), R.color.default_indicator_on);
                mItemUnselectedColor = ContextCompat.getColor(getContext(), R.color.default_indicator_off);
            } else {
                mItemSelectedColor = ContextCompat.getColor(getContext(), R.color.default_indicator_light_on);
                mItemUnselectedColor = ContextCompat.getColor(getContext(), R.color.default_indicator_light_off);
            }

            mItemSelectedColor = a.getColor(R.styleable.ViewPagerIndicator_itemSelectedColor, mItemSelectedColor);
            mItemUnselectedColor = a.getColor(R.styleable.ViewPagerIndicator_itemUnselectedColor, mItemUnselectedColor);

            mButtonDrawable = getSelectorDrawable();

            int drawableResId = a.getResourceId(R.styleable.ViewPagerIndicator_pagerIndicatorDrawable, 0);
            if (drawableResId != 0 ) {
                mButtonDrawable = ContextCompat.getDrawable(getContext(), drawableResId);
            }
        } catch(Exception e) {
            Log.e(TAG, getMessageFor(e));
        }

    }

    /**
     * Create a StateListDrawable for the pager indicator
     *
     * @return a proper StateListDrawable
     */
    private StateListDrawable getSelectorDrawable() {
        StateListDrawable d = null;

        try {
            d = new StateListDrawable();
            ShapeDrawable selectedDrawable = new ShapeDrawable(new OvalShape());
            selectedDrawable.getPaint().setColor(mItemSelectedColor);
            selectedDrawable.setIntrinsicHeight(mItemRadius * 2);
            selectedDrawable.setIntrinsicWidth(mItemRadius * 2);
            ShapeDrawable unselectedDrawable = new ShapeDrawable(new OvalShape());
            unselectedDrawable.getPaint().setColor(mItemUnselectedColor);
            unselectedDrawable.setIntrinsicHeight(mItemRadius * 2);
            unselectedDrawable.setIntrinsicWidth(mItemRadius * 2);

            d.addState(new int[]{android.R.attr.state_checked}, selectedDrawable);
            d.addState(new int[]{}, unselectedDrawable);
        } catch(Exception e) {
            Log.e(TAG, getMessageFor(e));
        }

        return d;
    }

    /**
     * Initialize ViewPagerIndicator with a properly set up ViewPager
     * @param viewPager an instance of android.support.v4.view.ViewPager
     * @throws IllegalStateException if no adapter has been provided to the viewPager
     */
    public void initWithViewPager(ViewPager viewPager) throws IllegalStateException {
        if ( viewPager == null ) return;

        if ( viewPager.getAdapter() == null ) throw new IllegalStateException("ViewPager has no adapter set.");

        try {
            mViewPager = viewPager;

            mViewPager.addOnPageChangeListener(mOnPageChangeListener);

            addViews();
        } catch(Exception e) {
            Log.e(TAG, getMessageFor(e));
        }
    }

    /**
     * Add page indicators based on the attached ViewPager
     */
    private void addViews() {
        try {
            if ( mViewPager == null || mViewPager.getAdapter() == null || mViewPager.getAdapter().getCount() == 0 ) return;
            removeAllViews();
            AppCompatRadioButton firstItem = new AppCompatRadioButton(getContext());
            firstItem.setText("");
            firstItem.setButtonDrawable(mButtonDrawable.getConstantState().newDrawable());
            ViewPagerIndicator.LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
            firstItem.setLayoutParams(params);
            firstItem.setClickable(false);
            addView(firstItem);
            for ( int i=1; i<mViewPager.getAdapter().getCount(); i++ ) {
                AppCompatRadioButton item = new AppCompatRadioButton(getContext());
                item.setText("");
                item.setButtonDrawable(mButtonDrawable.getConstantState().newDrawable());
                params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
                params.setMargins(mItemDividerWidth, 0, 0, 0);
                item.setLayoutParams(params);
                item.setClickable(false);
                addView(item);
            }
            check(firstItem.getId());
        } catch(Exception e) {
            Log.e(TAG, getMessageFor(e));
        }
    }

    private ViewPager.OnPageChangeListener mOnPageChangeListener = new ViewPager.OnPageChangeListener() {
        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}

        @Override
        public void onPageSelected(int position) {
            try {
                ViewPagerIndicator.this.check(ViewPagerIndicator.this.getChildAt(position).getId());
            } catch(Exception e) {
                Log.e(TAG, getMessageFor(e));
            }
        }

        @Override
        public void onPageScrollStateChanged(int state) {}
    };

    /**
     * Always get a message for an exception
     * @param e an Exception
     * @return a String describing the Exception
     */
    private String getMessageFor(Exception e) {
        if ( e == null ) return TAG + ": No Message.";

        return e != null && e.getMessage() != null ? e.getMessage() : e.getClass().getName() + ": No Message.";
    }
}


/*
Copyright (c) 2013, Groupon, Inc.
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:

Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.

Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.

Neither the name of GROUPON nor the names of its contributors may be
used to endorse or promote products derived from this software without
specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.groupon.uuid;

import java.lang.management.ManagementFactory;
import java.net.NetworkInterface;
import java.security.MessageDigest;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.Enumeration;
import java.util.Random;
import java.util.TimeZone;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * See README.md for more information. This is a UUID implementation that uses vB (custom) UUIDs by default, but can
 * parse and hold validly formatted UUIDs of any type. The content is stored as a byte array, and some effort has been
 * put into making this look at least a little bit like the java.util.UUID implementation. Probably the most common
 * uses of this class will be
 *
 * UUID id = new UUID();
 * to generate a new vB UUID
 *
 * UUID id = new UUID(uuidString);
 * to parse a 36-character string representation of a UUID
 *
 * String s = id.toString();
 * to serialize the UUID to a String
 */
public class UUID {
    public static final int PID                 = processId();
    public static final byte[] MAC              = macAddress();

    private static final int MAX_PID            = 65536;
    private static final int INCREMENT          = 198491317;
    private static final char VERSION           = 'b';
    private static final int VERSION_DEC        = mapToByte(VERSION, '0');
    private static final AtomicInteger COUNTER  = new AtomicInteger(new Random(System.nanoTime()).nextInt());
    private static final char[] HEX             =
            {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};

    private static boolean sequential           = false;
    protected final byte[] content;

    /**
     * Constructor that generates a new vB UUID using the current process id, MAC address, and timestamp.
     */
    public UUID() {
        long time = new Date().getTime();
        content = new byte[16];

        if (!sequential) {
            // atomically add a large prime number to the count and get the previous value
            int count = COUNTER.addAndGet(INCREMENT);

            // switch the order of the count in 4 bit segments and place into content
            content[0] = (byte) (((count & 0xF) << 4) | ((count & 0xF0) >> 4));
            content[1] = (byte) (((count & 0xF00) >> 4) | ((count & 0xF000) >> 12));
            content[2] = (byte) (((count & 0xF0000) >> 12) | ((count & 0xF00000) >> 20));
            content[3] = (byte) (((count & 0xF000000) >> 20) | ((count & 0xF0000000) >> 28));
        }
        else {
            int count = COUNTER.addAndGet(1);

            // get the count in order and place into content
            content[0] = (byte) (count >> 24);
            content[1] = (byte) (count >> 16);
            content[2] = (byte) (count >> 8);
            content[3] = (byte) (count);
        }

        // copy pid to content
        content[4]  = (byte) (PID >> 8);
        content[5]  = (byte) (PID);

        // place UUID version (hex 'b') in first four bits and piece of mac address in
        // the second four bits
        content[6]  = (byte) (VERSION_DEC | (0xF & MAC[2]));

        // copy rest of mac address into content
        content[7]  = MAC[3];
        content[8]  = MAC[4];
        content[9]  = MAC[5];

        // copy timestamp into content
        content[10] = (byte) (time >> 40);
        content[11] = (byte) (time >> 32);
        content[12] = (byte) (time >> 24);
        content[13] = (byte) (time >> 16);
        content[14] = (byte) (time >> 8);
        content[15] = (byte) (time);
    }

    /**
     * Constructor that takes a byte array as this UUID's content. This is equivalent to the binary representation
     * of the UUID. Note that the byte array is copied, so if the argument value changes then the constructed UUID
     * will not. This throws an IllegalArgumentException if the byte array is null or not 16 bytes long.
     * @param bytes UUID content as bytes.
     */
    public UUID(byte[] bytes) {
        if (bytes == null)
            throw new IllegalArgumentException("Tried to construct UUID with null byte array");

        if (bytes.length != 16)
            throw new IllegalArgumentException("Attempted to parse malformed UUID: " + Arrays.toString(bytes));

        content = Arrays.copyOf(bytes, 16);
    }

    /**
     * Constructor that takes a UUID string representation and parses it. This constructor expects the canonical UUID
     * String format validated by the isValidUUID() method and thros an IllegalArgumentException otherwise.
     * @param id UUID String representation, expected to be the valid UUID format.
     */
    public UUID(String id) {
        if (id == null)
            throw new IllegalArgumentException("Tried to construct UUID from null String");

        id = id.trim();
        char[] chars = id.toCharArray();

        if (!isValidUUID(chars))
            throw new IllegalArgumentException("Attempted to parse malformed UUID: " + id);

        content = new byte[16];
        content[0]  = mapToByte(chars[0],  chars[1]);
        content[1]  = mapToByte(chars[2],  chars[3]);
        content[2]  = mapToByte(chars[4],  chars[5]);
        content[3]  = mapToByte(chars[6],  chars[7]);
        content[4]  = mapToByte(chars[9],  chars[10]);
        content[5]  = mapToByte(chars[11], chars[12]);
        content[6]  = mapToByte(chars[14], chars[15]);
        content[7]  = mapToByte(chars[16], chars[17]);
        content[8]  = mapToByte(chars[19], chars[20]);
        content[9]  = mapToByte(chars[21], chars[22]);
        content[10] = mapToByte(chars[24], chars[25]);
        content[11] = mapToByte(chars[26], chars[27]);
        content[12] = mapToByte(chars[28], chars[29]);
        content[13] = mapToByte(chars[30], chars[31]);
        content[14] = mapToByte(chars[32], chars[33]);
        content[15] = mapToByte(chars[34], chars[35]);
    }

    /**
     * Constructs a Locality UUID from a standard Java java.util.UUID object. A java.util.UUID object won't return
     * its content as a byte array, but will return the first and second halves of content as longs. We use these
     * to construct a new Locality UUID object. This throws an IllegalArgumentException if the uuid argument is null.
     * @param uuid A non-null java.util.UUID object, from which we will construct a locality UUID with identical content.
     */
    public UUID(java.util.UUID uuid) {
        if (uuid == null)
            throw new IllegalArgumentException("Tried to construct Locality UUID with null java.util.UUID");

        content = new byte[16];
        constructFromLongs(uuid.getMostSignificantBits(), uuid.getLeastSignificantBits());
    }

    /**
     * Constructs a UUID using long values representing the first and second half of the UUID content. This was added
     * to be consistent with the java.util.UUID constructor.
     * @param mostSigBits Long value representing the first half of the UUID.
     * @param leastSigBits Long value representing the second half of the UUID.
     */
    public UUID(long mostSigBits, long leastSigBits) {
        content = new byte[16];
        constructFromLongs(mostSigBits, leastSigBits);
    }

    /**
     * This method sets the content based on the values of two longs representing the first and second half of the UUID.
     * This method is called from the UUID(java.util.UUID) and UUID(long, long) constructors.
     * @param hi Long value representing the first half of the UUID.
     * @param lo Long value representing the second half of the UUID.
     */
    private void constructFromLongs(long hi, long lo) {
        content[ 0] = (byte) (hi >> 56);
        content[ 1] = (byte) (hi >> 48);
        content[ 2] = (byte) (hi >> 40);
        content[ 3] = (byte) (hi >> 32);
        content[ 4] = (byte) (hi >> 24);
        content[ 5] = (byte) (hi >> 16);
        content[ 6] = (byte) (hi >>  8);
        content[ 7] = (byte) (hi      );
        content[ 8] = (byte) (lo >> 56);
        content[ 9] = (byte) (lo >> 48);
        content[10] = (byte) (lo >> 40);
        content[11] = (byte) (lo >> 32);
        content[12] = (byte) (lo >> 24);
        content[13] = (byte) (lo >> 16);
        content[14] = (byte) (lo >>  8);
        content[15] = (byte) (lo      );
    }

    /**
     * This method validates a UUID String by making sure its non-null and calling isValidUUID(char[]).
     * @param id UUID String.
     * @return True or false based on whether the String can be used to construct a UUID.
     */
    public static boolean isValidUUID(String id) {
        return id != null && isValidUUID(id.toCharArray());
    }

    /**
     * This method validates a character array as the expected format for a printed representation of a UUID. The
     * expected format is 36 characters long, with '-' at the 8th, 13th, 18th, and 23rd characters. The remaining
     * characters are expected to be valid hex, meaning in the range ('0' - '9', 'a' - 'f', 'A' - 'F') inclusive.
     * If a character array is valid, then it can be used to construct a UUID. This method has been written unrolled
     * and verbosely, with the theory that this is simpler and faster than using loops or a regex.
     * @param ch A character array of a UUID's printed representation.
     * @return True or false based on whether the UUID is valid, no exceptions are thrown.
     */
    public static boolean isValidUUID(char[] ch) {
        return  ch != null       &&
                ch.length == 36  &&
                validHex(ch[0 ]) &&
                validHex(ch[1 ]) &&
                validHex(ch[2 ]) &&
                validHex(ch[3 ]) &&
                validHex(ch[4 ]) &&
                validHex(ch[5 ]) &&
                validHex(ch[6 ]) &&
                validHex(ch[7 ]) &&
                ch[8] == '-'     &&
                validHex(ch[9 ]) &&
                validHex(ch[10]) &&
                validHex(ch[11]) &&
                validHex(ch[12]) &&
                ch[13] == '-'    &&
                validHex(ch[14]) &&
                validHex(ch[15]) &&
                validHex(ch[16]) &&
                validHex(ch[17]) &&
                ch[18] == '-'    &&
                validHex(ch[19]) &&
                validHex(ch[20]) &&
                validHex(ch[21]) &&
                validHex(ch[22]) &&
                ch[23] == '-'    &&
                validHex(ch[24]) &&
                validHex(ch[25]) &&
                validHex(ch[26]) &&
                validHex(ch[27]) &&
                validHex(ch[28]) &&
                validHex(ch[29]) &&
                validHex(ch[30]) &&
                validHex(ch[31]) &&
                validHex(ch[32]) &&
                validHex(ch[33]) &&
                validHex(ch[34]) &&
                validHex(ch[35]);
    }

    /**
     * Just a simple method to determine if a character is valid hex in the range ('0' - '9', 'a' - 'f', 'A' - 'F').
     * @param c A character to test.
     * @return True or false based on whether or not the character is in the expected range.
     */
    private static boolean validHex(char c) {
        return  (c >= '0' && c <= '9') ||
                (c >= 'a' && c <= 'f') ||
                (c >= 'A' && c <= 'F');
    }

    /**
     * Toggle UUID generator into sequential mode, so the random segment is in order and increases by one. In
     * sequential mode, there is presumably a desire that UUIDs generated around the same time should begin with similar
     * characters, but this is difficult in a distributed environment. The solution is to set the counter value based
     * on a hash of the UTC date and time up to a 10 minute precision. This means that UUID classes initialized at
     * similar times should start with similar counter values, but this is not guaranteed. If one of these classes
     * is generating vastly more UUIDs than others, then these counters can become skewed.
     *
     * Calling this method more than once without toggling back to variable mode has no effect, so it probably makes
     * more sense to call this from a static context, like your main method or in a class' static initialization.
     */
    public static void useSequentialIds() {
        if (!sequential) {
            // get string that changes every 10 minutes
            TimeZone tz = TimeZone.getTimeZone("UTC");
            DateFormat df = new SimpleDateFormat("yyyyMMddHHmm");
            df.setTimeZone(tz);
            String date = df.format(new Date()).substring(0, 11);

            // run an md5 hash of the string, no reason this needs to be secure
            byte[] digest;
            try {
                MessageDigest md = MessageDigest.getInstance("MD5");
                digest = md.digest(date.getBytes("UTF-8"));
            }
            catch (Exception e) {
                throw new RuntimeException("Could not create hash of date for the sequential counter", e);
            }

            // create integer from first 4 bytes of md5 hash
            int x;
            x  = ((int)digest[0] & 0xFF);
            x |= ((int)digest[1] & 0xFF) << 8;
            x |= ((int)digest[2] & 0xFF) << 16;
            x |= ((int)digest[3] & 0xFF) << 24;
            COUNTER.set(x);
        }
        sequential = true;
    }

    /**
     * Toggle uuid generator into variable mode, so the random segment is in reverse order and
     * increases by a large increment. This is the default mode.
     */
    public static void useVariableIds() {
        sequential = false;
    }

    /**
     * This method maps a hex character to its 4-bit representation in an int.
     * @param x Hex character in the range ('0' - '9', 'a' - 'f', 'A' - 'F').
     * @return 4-bit number in int representing hex offset from 0.
     */
    private static int intValue(char x) {
        if (x >= '0' && x <= '9')
            return x - '0';
        if (x >= 'a' && x <= 'f')
            return x - 'a' + 10;
        if (x >= 'A' && x <= 'F')
            return x - 'A' + 10;
        throw new RuntimeException("Error parsing UUID at character: " + x);
    }

    /**
     * Map two hex characters to 4-bit numbers and combine them to produce 8-bit number in byte.
     * @param a First hex character.
     * @param b Second hex character.
     * @return Byte representation of given hex characters.
     */
    private static byte mapToByte(char a, char b) {
        int ai = intValue(a);
        int bi = intValue(b);
        return (byte) ((ai << 4) | bi);
    }

    /**
     * Get contents of this UUID as a byte array. The array is copied before returning so that it can't be changed.
     * @return Raw byte array of UUID contents.
     */
    public byte[] getBytes() {
        return Arrays.copyOf(content, 16);
    }

    /**
     * Get this UUID object as a String. It will be returned as a canonical 36-character UUID string with lower-case
     * hex characters.
     * @return UUID as String.
     */
    @Override
    public String toString() {
        char[] id = new char[36];

        // split each byte into 4 bit numbers and map to hex characters
        id[0]  = HEX[(content[0]  & 0xF0) >> 4];
        id[1]  = HEX[(content[0]  & 0x0F)];
        id[2]  = HEX[(content[1]  & 0xF0) >> 4];
        id[3]  = HEX[(content[1]  & 0x0F)];
        id[4]  = HEX[(content[2]  & 0xF0) >> 4];
        id[5]  = HEX[(content[2]  & 0x0F)];
        id[6]  = HEX[(content[3]  & 0xF0) >> 4];
        id[7]  = HEX[(content[3]  & 0x0F)];
        id[8]  = '-';
        id[9]  = HEX[(content[4]  & 0xF0) >> 4];
        id[10] = HEX[(content[4]  & 0x0F)];
        id[11] = HEX[(content[5]  & 0xF0) >> 4];
        id[12] = HEX[(content[5]  & 0x0F)];
        id[13] = '-';
        id[14] = HEX[(content[6]  & 0xF0) >> 4];
        id[15] = HEX[(content[6]  & 0x0F)];
        id[16] = HEX[(content[7]  & 0xF0) >> 4];
        id[17] = HEX[(content[7]  & 0x0F)];
        id[18] = '-';
        id[19] = HEX[(content[8]  & 0xF0) >> 4];
        id[20] = HEX[(content[8]  & 0x0F)];
        id[21] = HEX[(content[9]  & 0xF0) >> 4];
        id[22] = HEX[(content[9]  & 0x0F)];
        id[23] = '-';
        id[24] = HEX[(content[10] & 0xF0) >> 4];
        id[25] = HEX[(content[10] & 0x0F)];
        id[26] = HEX[(content[11] & 0xF0) >> 4];
        id[27] = HEX[(content[11] & 0x0F)];
        id[28] = HEX[(content[12] & 0xF0) >> 4];
        id[29] = HEX[(content[12] & 0x0F)];
        id[30] = HEX[(content[13] & 0xF0) >> 4];
        id[31] = HEX[(content[13] & 0x0F)];
        id[32] = HEX[(content[14] & 0xF0) >> 4];
        id[33] = HEX[(content[14] & 0x0F)];
        id[34] = HEX[(content[15] & 0xF0) >> 4];
        id[35] = HEX[(content[15] & 0x0F)];

        return new String(id);
    }

    /**
     * Get the most significant bits (the first half) of the UUID content as a 64-bit long.
     * @return The first half of the UUID as a long.
     */
    public long getMostSignificantBits() {
        long a;
        a  = ((long)content[ 0] & 0xFF) << 56;
        a |= ((long)content[ 1] & 0xFF) << 48;
        a |= ((long)content[ 2] & 0xFF) << 40;
        a |= ((long)content[ 3] & 0xFF) << 32;
        a |= ((long)content[ 4] & 0xFF) << 24;
        a |= ((long)content[ 5] & 0xFF) << 16;
        a |= ((long)content[ 6] & 0xFF) << 8;
        a |= ((long)content[ 7] & 0xFF);
        return a;
    }

    /**
     * Get the least significant bits (the second half) of the UUID content as a 64-bit long.
     * @return The second half of the UUID as a long.
     */
    public long getLeastSignificantBits() {
        long b;
        b  = ((long)content[ 8] & 0xFF) << 56;
        b |= ((long)content[ 9] & 0xFF) << 48;
        b |= ((long)content[10] & 0xFF) << 40;
        b |= ((long)content[11] & 0xFF) << 32;
        b |= ((long)content[12] & 0xFF) << 24;
        b |= ((long)content[13] & 0xFF) << 16;
        b |= ((long)content[14] & 0xFF) << 8;
        b |= ((long)content[15] & 0xFF);
        return b;
    }

    /**
     * Get the java.util.UUID representation of this UUID object. The java.util.UUID is constructed using the most and
     * least significant bits of this UUID's content. No memory is shared with the new java.util.UUID.
     * @return This com.groupon.uuid.UUID's representation as a java.util.UUID.
     */
    public java.util.UUID toJavaUUID() {
        return new java.util.UUID(getMostSignificantBits(), getLeastSignificantBits());
    }

    /**
     * Extract version field as a hex char from raw UUID bytes. By default, generated UUIDs will have 'b' as the
     * version, but it is possible to parse UUIDs of different types, '4' for example.
     * @return UUID version as a char.
     */
    public char getVersion() {
        return HEX[(content[6] & 0xF0) >> 4];
    }

    /**
     * Extract process id from raw UUID bytes and return as int. This only applies for this type of UUID, for other
     * UUID types, such as the randomly generated v4, its not possible to discover process id, so -1 is returned.
     * @return Id of process that generated the UUID, or -1 for unrecognized format.
     */
    public int getProcessId() {
        if (getVersion() != VERSION)
            return -1;

        return ((content[4] & 0xFF) << 8) | (content[5] & 0xFF);
    }

    /**
     * Extract timestamp from raw UUID bytes and return as int. If the UUID is not the default type, then we can't parse
     * the timestamp out and null is returned.
     * @return Millisecond UTC timestamp from generation of the UUID, or null for unrecognized format.
     */
    public Date getTimestamp() {
        if (getVersion() != VERSION)
            return null;

        long time;
        time  = ((long)content[10] & 0xFF) << 40;
        time |= ((long)content[11] & 0xFF) << 32;
        time |= ((long)content[12] & 0xFF) << 24;
        time |= ((long)content[13] & 0xFF) << 16;
        time |= ((long)content[14] & 0xFF) << 8;
        time |= ((long)content[15] & 0xFF);
        return new Date(time);
    }

    /**
     * Extract MAC address fragment from raw UUID bytes, setting missing values to 0, thus the first 2 and a half bytes
     * will be 0, followed by 3 and a half bytes of the active MAC address when the UUID was generated.
     * @return Byte array of UUID fragment, or null for unrecognized format.
     */
    public byte[] getMacFragment() {
        if (getVersion() != 'b')
            return null;

        byte[] x = new byte[6];

        x[0] = 0;
        x[1] = 0;
        x[2] = (byte) (content[6] & 0xF);
        x[3] = content[7];
        x[4] = content[8];
        x[5] = content[9];

        return x;
    }

    /**
     * Basic implementation of equals that checks if the given object is null or a different type, then compares the
     * byte arrays which store the content of the UUID. I've considered making this compatible with the content of
     * java.util.UUID, but not sure thats a good idea given that those are objects of a different type, and doing
     * a deep comparison might be surprising functionality.
     * @param o Object against which we compare this UUID.
     * @return True or false if this UUID's content is identical to the given UUID.
     */
    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (o == null || getClass() != o.getClass())
            return false;

        UUID that = (UUID) o;

        if (this.content.length != that.content.length)
            return false;

        for (int i = 0; i < this.content.length; i++)
            if (this.content[i] != that.content[i])
                return false;

        return true;
    }

    /**
     * The hash code implementation just calls Arrays.hashCode() on the contents of this UUID (stored as a byte array).
     * @return The hash value of this object.
     */
    @Override
    public int hashCode() {
        return Arrays.hashCode(content);
    }

    /**
     * Get the active MAC address on the current machine as a byte array. This is called when generating a new UUID.
     * Note that a machine can have multiple or no active MAC addresses. This method works by iterating through the list
     * of network interfaces, ignoring the loopback interface and any virtual interfaces (which often have made-up
     * addresses), and returning the first one we find. If no valid addresses are found, then a byte array of the same
     * length with all zeros is returned.
     * @return 6-byte array for first active MAC address, or 6-byte zeroed array if no interfaces are active.
     */
    private static byte[] macAddress() {
        try {
            Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
            byte[] mac = null;

            while (interfaces.hasMoreElements() && (mac == null || mac.length != 6)) {
                NetworkInterface netInterface = interfaces.nextElement();
                if (netInterface.isLoopback() || netInterface.isVirtual())
                    continue;
                mac = netInterface.getHardwareAddress();
            }

            // if the machine is not connected to a network it has no active MAC address
            if (mac == null)
                mac = new byte[] {0, 0, 0, 0, 0, 0};

            return mac;
        } catch (Exception e) {
            throw new RuntimeException("Could not get MAC address");
        }
    }

    /**
     * Get the process id of this JVM. I haven't tested this extensively so its possible that this performs differently
     * on esoteric JVMs. I copied this from:
     * http://stackoverflow.com/questions/35842/how-can-a-java-program-get-its-own-process-id
     * @return Id of the current JVM process.
     */
    private static int processId() {
        // Note: may fail in some JVM implementations
        // something like '<pid>@<hostname>', at least in SUN / Oracle JVMs
        final String jvmName = ManagementFactory.getRuntimeMXBean().getName();
        final int index = jvmName.indexOf('@');

        if (index < 1)
            throw new RuntimeException("Could not get PID");

        try {
            return Integer.parseInt(jvmName.substring(0, index)) % MAX_PID;
        } catch (NumberFormatException e) {
            throw new RuntimeException("Could not get PID");
        }
    }
}


/*
 * Copyright (c) 2012 Socialize Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.socialize;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import com.socialize.android.ioc.IOCContainer;
import com.socialize.android.ioc.Logger;
import com.socialize.api.SocializeSession;
import com.socialize.api.action.ShareType;
import com.socialize.api.action.share.ShareSystem;
import com.socialize.api.action.user.UserSystem;
import com.socialize.auth.AuthProvider;
import com.socialize.auth.AuthProviderInfo;
import com.socialize.auth.AuthProviderInfoBuilder;
import com.socialize.auth.AuthProviderType;
import com.socialize.auth.AuthProviders;
import com.socialize.auth.SocializeAuthProviderInfo;
import com.socialize.auth.UserProviderCredentials;
import com.socialize.auth.UserProviderCredentialsMap;
import com.socialize.concurrent.AsyncTaskManager;
import com.socialize.concurrent.ManagedAsyncTask;
import com.socialize.config.SocializeConfig;
import com.socialize.entity.Comment;
import com.socialize.error.SocializeException;
import com.socialize.init.SocializeInitializationAsserter;
import com.socialize.ioc.SocializeIOC;
import com.socialize.listener.SocializeAuthListener;
import com.socialize.listener.SocializeInitListener;
import com.socialize.listener.SocializeListener;
import com.socialize.location.SocializeLocationProvider;
import com.socialize.log.SocializeLogger;
import com.socialize.loopy.LoopyService;
import com.socialize.networks.facebook.FacebookFacade;
import com.socialize.networks.facebook.FacebookUtils;
import com.socialize.networks.twitter.TwitterUtils;
import com.socialize.notifications.C2DMCallback;
import com.socialize.notifications.NotificationChecker;
import com.socialize.notifications.SocializeC2DMReceiver;
import com.socialize.notifications.WakeLock;
import com.socialize.ui.ActivityIOCProvider;
import com.socialize.ui.SocializeEntityLoader;
import com.socialize.util.AppUtils;
import com.socialize.util.ClassLoaderProvider;
import com.socialize.util.DisplayUtils;
import com.socialize.util.EntityLoaderUtils;
import com.socialize.util.ResourceLocator;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * @author Jason Polites
 */
public class SocializeServiceImpl implements SocializeService {

	static final String receiver = SocializeC2DMReceiver.class.getName();
	static final Lock initLock = new ReentrantReadWriteLock().writeLock();

	private SocializeLogger logger;
	private IOCContainer container;
	private SocializeSession session;
	private AuthProviderInfoBuilder authProviderInfoBuilder;
	private SocializeInitializationAsserter asserter;
	private ShareSystem shareSystem;
	private UserSystem userSystem;
    private LoopyService loopyService;

	private AuthProviders authProviders;
	private NotificationChecker notificationChecker;
	private AppUtils appUtils;
	private SocializeLocationProvider locationProvider;

	private SocializeSystem system = SocializeSystem.getInstance();
	private SocializeConfig config = new SocializeConfig();

	private SocializeEntityLoader entityLoader;

	private Class<?> userSettingsActivity;

	private String[] initPaths = null;
	private int initCount = 0;

	private boolean paused = false;

	public SocializeServiceImpl() {
		super();
		this.logger = newLogger();
	}

	@Override
	public Class<?> getUserSettingsActivity() {
		return userSettingsActivity;
	}

	@Override
	public void setUserSettingsActivity(Class<?> userSettingsActivity) {
		this.userSettingsActivity = userSettingsActivity;
	}

	@Override
	public boolean handleBroadcastIntent(Context context, Intent intent) {

		String action = intent.getAction();

		if(action != null && (SocializeC2DMReceiver.C2DM_INTENT.equals(action) || SocializeC2DMReceiver.REGISTRATION_CALLBACK_INTENT.equals(action))) {

			if(SocializeC2DMReceiver.C2DM_INTENT.equals(action)) {
				// Check the bundle data for source
				Bundle extras = intent.getExtras();
				if(extras != null) {
					String source = extras.getString(C2DMCallback.SOURCE_KEY);
					if(source != null && source.trim().equalsIgnoreCase(C2DMCallback.SOURCE_SOCIALIZE)) {
						handleIntent(context, intent);
						return true;
					}
				}
			}
			else {
				// Handle registration, but don't return true.
				handleIntent(context, intent);
			}
		}

		return false;
	}

	protected void handleIntent(Context context, Intent intent) {
		getWakeLock().acquire(context);
		intent.setClassName(context, receiver);
		context.startService(intent);
	}

	// So we can mock
	protected WakeLock getWakeLock() {
		return WakeLock.getInstance();
	}

	@Override
	public boolean isSupported(Context context, AuthProviderType type) {
		switch(type) {
			case FACEBOOK:
				return FacebookUtils.isAvailable(context);
			case TWITTER:
				return TwitterUtils.isAvailable(context);
			default:
				return authProviderInfoBuilder.isSupported(type);
		}
	}

	@Override
	public void isSocializeSupported(Context context) throws SocializeException {
		// Check that we are not LDPI
		DisplayUtils displayUtils = new DisplayUtils();
		displayUtils.init(context);
		if(displayUtils.isLDPI()) {
			throw new SocializeException("Socialize is not supported on low resolution (LDPI) devices");
		}
	}

	/* (non-Javadoc)
	 * @see com.socialize.SocializeService#init(android.content.Context)
	 */
	@Override
	public IOCContainer init(Context context) {
		return init(context, getSystem().getBeanConfig(context));
	}

	/* (non-Javadoc)
	 * @see com.socialize.SocializeService#init(android.content.Context, java.lang.String)
	 */
	@Override
	public IOCContainer init(Context context, String...paths) {
		try {
			return initWithContainer(context, paths);
		}
		catch (Exception e) {
			if(logger != null) {
				logger.error(SocializeLogger.INITIALIZE_FAILED, e);
			}
			else {
				SocializeLogger.e(e.getMessage(), e);
			}
		}

		return null;
	}

	/*
	 * (non-Javadoc)
	 * @see com.socialize.SocializeService#initAsync(android.content.Context, com.socialize.listener.SocializeInitListener)
	 */
	@Override
	public void initAsync(Context context, SocializeInitListener listener) {
		initAsync(context, listener, getSystem().getBeanConfig(context));
	}

	/*
	 * (non-Javadoc)
	 * @see com.socialize.SocializeService#initAsync(android.content.Context, com.socialize.listener.SocializeInitListener, java.lang.String[])
	 */
	@Override
	public void initAsync(Context context, SocializeInitListener listener, String... paths) {
		new InitTask(this, context, paths, listener, logger).execute();
	}

	public synchronized IOCContainer initWithContainer(Context context, String...paths) throws Exception {
		return initWithContainer(context, getSystem().getSystemInitListener(), paths);
	}

	public synchronized IOCContainer initWithContainer(Context context, SocializeInitListener listener, String...paths) throws Exception {

		try {
			initLock.lock();

			boolean init = false;

			// Check socialize is supported on this device.
			isSocializeSupported(context);

			String[] localPaths = getInitPaths();

			if(paths != null) {

				if(isInitialized(context)) {

					if(localPaths != null) {
						for (String path : paths) {
							if(binarySearch(localPaths, path) < 0) {

								if(logger != null && logger.isInfoEnabled()) {
									logger.info("New path found for beans [" +
											path +
											"].  Re-initializing Socialize");
								}

								this.initCount = 0;

								// Destroy the container so we recreate bean references
								if(container != null) {
									if(logger != null && logger.isDebugEnabled()) {
										logger.debug("Destroying IOC container");
									}
									container.destroy();
								}

								init = true;

								break;
							}
						}
					}
					else {
						String msg = "Socialize reported as initialize, but no initPaths were found.  This should not happen!";
						if(logger != null) {
							logger.error(msg);
						}
						else {
							System.err.println(msg);
						}

						destroy();
						init = true;
					}
				}
				else {
					init = true;
				}

				if(init) {
					Logger.LOG_KEY = Socialize.LOG_KEY;
					Logger.logLevel = Log.WARN;

					this.initPaths = paths;

					// JP:  This sort is in the wrong place, should only be used when searching for new paths.
//				    sort(this.initPaths);

					if(container == null) {
						container = newSocializeIOC();
					}

					ResourceLocator locator = newResourceLocator();

					locator.setLogger(newLogger());

					ClassLoaderProvider provider = newClassLoaderProvider();

					locator.setClassLoaderProvider(provider);

					if(logger != null) {

						if(logger.isDebugEnabled()) {
							for (String path : paths) {
								logger.debug("Initializing Socialize with path [" +
										path +
										"]");
							}

							Logger.logLevel = Log.DEBUG;
						}
						else if(logger.isInfoEnabled()) {
							Logger.logLevel = Log.INFO;
						}
					}

					((SocializeIOC) container).init(context, locator, paths);

					init(context, container, listener); // initCount incremented here
				}
				else {
					this.initCount++;
				}

                // Always set the context on the container
                setContext(context);
			}
			else {
				String msg = "Attempt to initialize Socialize with null bean config paths";
				if(logger != null) {
					logger.error(msg);
				}
				else {
					System.err.println(msg);
				}
			}
		}
		finally {
			initLock.unlock();
		}

		return container;
	}

	protected void setContext(Context context) {
		if(container != null) {
			container.setContext(context);
		}
	}

    protected void onContextDestroyed(Context context) {
		if(container != null) {
			container.onContextDestroyed(context);
		}
	}

	// So we can mock
	protected String[] getInitPaths() {
		return initPaths;
	}

	// So we can mock
	protected SocializeIOC newSocializeIOC() {
		return new SocializeIOC();
	}

	// So we can mock
	protected ResourceLocator newResourceLocator() {
		return new ResourceLocator();
	}

	// So we can mock
	protected SocializeLogger newLogger() {
		return new SocializeLogger(Socialize.DEFAULT_LOG_LEVEL);
	}

	// So we can mock
	protected ClassLoaderProvider newClassLoaderProvider() {
		return new ClassLoaderProvider();
	}

	// So we can mock
	protected int binarySearch(String[] array, String str) {
		// No longer a binary search because we don't want the list to be sorted as this distrubs the priority
		for (int i = 0; i < array.length; i++) {
			if(array[i].equals(str)) {
				return i;
			}
		}
		return -1;
	}

	/* (non-Javadoc)
	 * @see com.socialize.SocializeService#init(android.content.Context, com.socialize.android.ioc.IOCContainer)
	 */
	@Override
	public synchronized void init(Context context, final IOCContainer container) {
		init(context, container, getSystem().getSystemInitListener());
	}

	public synchronized void init(Context context, final IOCContainer container, SocializeInitListener listener) {
		if(!isInitialized(context)) {
			try {
				this.container = container;
				this.container.setContext(context);

				this.logger = container.getBean("logger");
                this.loopyService = container.getBean("loopyService");

				this.shareSystem = container.getBean("shareSystem");
				this.userSystem = container.getBean("userSystem");
				this.asserter = container.getBean("initializationAsserter");

				if(this.authProviders == null) {
					this.authProviders = container.getBean("authProviders");
				}

				this.authProviderInfoBuilder = container.getBean("authProviderInfoBuilder");
				this.notificationChecker = container.getBean("notificationChecker");
				this.appUtils = container.getBean("appUtils");
				this.locationProvider = container.getBean("locationProvider");

				SocializeConfig mainConfig = container.getBean("config");

				mainConfig.merge(config);
				mainConfig.merge(ConfigUtils.preInitConfig);

				this.config = mainConfig;
				this.initCount=1;

				verify3rdPartyAuthConfigured();

				// Create the entity loader if we have one
				initEntityLoader();

				// Check we are configured ok
				appUtils.checkAndroidManifest(context);

				ActivityIOCProvider.getInstance().setContainer(container);

				initNotifications(context);

				if(listener != null) {
					listener.onInit(context, container);
				}
			}
			catch (Exception e) {
				if(logger != null) {
					logger.error(SocializeLogger.INITIALIZE_FAILED, e);
				}
				else {
					SocializeLogger.e(e.getMessage(), e);
				}
			}
		}
		else {
			this.initCount++;
		}
	}

	protected synchronized void initNotifications(Context context) {
		if(config.isNotificationsEnabled()) {
			if(notificationChecker != null) {
				notificationChecker.checkRegistrations(context);
			}
		}
	}

	protected synchronized void initEntityLoader() {
		EntityLoaderUtils entityLoaderUtils = container.getBean("entityLoaderUtils");
		entityLoaderUtils.initEntityLoader();
	}

	protected void verify3rdPartyAuthConfigured() {
		authProviderInfoBuilder.validateAll();
	}

	@Override
	public void saveSession(Context context) {
		userSystem.saveSession(context, session);
	}

	@Override
	public void clear3rdPartySession(Context context, AuthProviderType type) {
		try {
			if(session != null) {
				AuthProvider<AuthProviderInfo> provider = authProviders.getProvider(type);

				if(provider != null) {
					UserProviderCredentials userProviderCredentials = session.getUserProviderCredentials(type);

					if(userProviderCredentials != null) {
						AuthProviderInfo authProviderInfo = userProviderCredentials.getAuthProviderInfo();
						if(authProviderInfo != null) {
							provider.clearCache(context, authProviderInfo);
						}
					}
				}

				session.clear(type);
			}
		}
		finally {
			if(userSystem != null) {
				userSystem.clearSession(type);
			}
		}
	}

	@Override
	public void clearSessionCache(Context context) {
		try {
			if(session != null) {

				UserProviderCredentialsMap userProviderCredentialsMap = session.getUserProviderCredentials();

				if(userProviderCredentialsMap != null) {
					Collection<UserProviderCredentials> values = userProviderCredentialsMap.values();
					for (UserProviderCredentials userProviderCredentials : values) {
						AuthProviderInfo authProviderInfo = userProviderCredentials.getAuthProviderInfo();
						if(authProviderInfo != null) {
							clear3rdPartySession(context, authProviderInfo.getType());
						}
					}
				}

				session = null;
			}
		}
		finally {
			if(userSystem != null) {
				userSystem.clearSession();
			}
		}
	}

	/* (non-Javadoc)
	 * @see com.socialize.SocializeService#destroy()
	 */
	@Override
	public void destroy() {
		initCount--;

		if(initCount <= 0) {
			destroy(true);
		}
	}

	/*
	 * (non-Javadoc)
	 * @see com.socialize.SocializeService#destroy(boolean)
	 */
	@Override
	public synchronized void destroy(boolean force) {
		if(force) {

			if(AsyncTaskManager.isManaged()) {
				AsyncTaskManager.terminateAll(10, TimeUnit.SECONDS);
			}

			if(container != null) {
				if(logger != null && logger.isDebugEnabled()) {
					logger.debug("Destroying IOC container");
				}
				container.destroy();
			}

			config.destroy();
			system.destroy();
			initCount = 0;
			initPaths = null;
			entityLoader = null;
			session = null;
			authProviders = null;
		}
		else {
			destroy();
		}
	}

	@Override
	public synchronized void authenticate(Context context, String consumerKey, String consumerSecret, AuthProviderInfo authProviderInfo, SocializeAuthListener authListener) {
		if(assertInitialized(context, authListener)) {
			userSystem.authenticate(context, consumerKey, consumerSecret, authProviderInfo, authListener, this);
		}
	}

	/*
	 * (non-Javadoc)
	 * @see com.socialize.SocializeService#authenticate(android.content.Context, com.socialize.listener.SocializeAuthListener)
	 */
	@Override
	public synchronized void authenticate(Context context, SocializeAuthListener authListener) {
		if(assertInitialized(context, authListener)) {
			userSystem.authenticate(context, authListener, this);
		}
	}

	public synchronized SocializeSession authenticateSynchronous(final Context context) throws SocializeException {

		if(userSystem == null) {
			initCount = 0;
			init(context);
		}

		if(userSystem != null) {

			final CountDownLatch latch = new CountDownLatch(1);
			final List<Exception> holder = new ArrayList<Exception>(1);

			new Thread() {
				@Override
				public void run() {
					try {
						SocializeSession session = userSystem.authenticateSynchronous(context);
						setSession(session);
						latch.countDown();
					}
					catch (Exception e) {
						holder.add(e);
						latch.countDown();
					}
				}
			}.start();

			try {
				if(!latch.await(20, TimeUnit.SECONDS)) {
					throw new SocializeException("Timeout while authenticating");
				}
			}
			catch (InterruptedException ignore) {}

			if(!holder.isEmpty()) {
				throw SocializeException.wrap(holder.get(0));
			}

			return session;
		}

		throw new SocializeException("Socialize not initialized");
	}

	@Deprecated
	@Override
	public void authenticate(Context context, AuthProviderType authProviderType, SocializeAuthListener authListener, String... permissions) {
		SocializeConfig config = getConfig();
		String consumerKey = config.getProperty(SocializeConfig.SOCIALIZE_CONSUMER_KEY);
		String consumerSecret = config.getProperty(SocializeConfig.SOCIALIZE_CONSUMER_SECRET);

		if(permissions.length > 0) {
			if(!Arrays.equals(permissions, FacebookFacade.DEFAULT_PERMISSIONS)) {
				// Ensure the requested permissions include the default permissions
				Set<String> all = new HashSet<String>();
				all.addAll(Arrays.asList(permissions));
				all.addAll(Arrays.asList(FacebookFacade.DEFAULT_PERMISSIONS));
				permissions = all.toArray(new String[all.size()]);
			}
		}

		AuthProviderInfo authProviderInfo = authProviderInfoBuilder.getFactory(authProviderType).getInstance(permissions);

		authenticate(context, consumerKey, consumerSecret, authProviderInfo,  authListener);
	}

	@Override
	public void authenticateForRead(Context context, AuthProviderType authProviderType, SocializeAuthListener authListener, String... permissions) {
		SocializeConfig config = getConfig();
		String consumerKey = config.getProperty(SocializeConfig.SOCIALIZE_CONSUMER_KEY);
		String consumerSecret = config.getProperty(SocializeConfig.SOCIALIZE_CONSUMER_SECRET);
		AuthProviderInfo authProviderInfo = authProviderInfoBuilder.getFactory(authProviderType).getInstanceForRead(permissions);
		authenticate(context, consumerKey, consumerSecret, authProviderInfo,  authListener);
	}

	@Override
	public void authenticateForWrite(Context context, AuthProviderType authProviderType, SocializeAuthListener authListener, String... permissions) {
		SocializeConfig config = getConfig();
		String consumerKey = config.getProperty(SocializeConfig.SOCIALIZE_CONSUMER_KEY);
		String consumerSecret = config.getProperty(SocializeConfig.SOCIALIZE_CONSUMER_SECRET);
		AuthProviderInfo authProviderInfo = authProviderInfoBuilder.getFactory(authProviderType).getInstanceForWrite(permissions);
		authenticate(context, consumerKey, consumerSecret, authProviderInfo,  authListener);
	}

	@Override
	public synchronized void authenticate(Context context, String consumerKey, String consumerSecret, SocializeAuthListener authListener) {
		if(assertInitialized(context, authListener)) {
			userSystem.authenticate(context, consumerKey, consumerSecret, authListener, this);
		}
	}

	// So we can mock
	protected SocializeAuthProviderInfo newSocializeAuthProviderInfo() {
		return new SocializeAuthProviderInfo();
	}

	@Override
	public void authenticateKnownUser(Context context, UserProviderCredentials userProviderCredentials, SocializeAuthListener authListener) {
		if(assertInitialized(context, authListener)) {
			userSystem.authenticateKnownUser(context, userProviderCredentials, authListener, this);
		}
	}

	protected void logError(String message, Throwable error) {
		if(logger != null) {
			logger.error(message, error);
		}
		else {
			SocializeLogger.e(message, error);
		}
	}

	protected void logErrorMessage(String message) {
		if(logger != null) {
			logger.error(message);
		}
		else {
			System.err.println(message);
		}
	}

	// So we can mock
	protected Comment newComment() {
		return new Comment();
	}

	@Deprecated
	public boolean isInitialized() {
		return this.initCount > 0;
	}

	@Override
	public boolean isInitialized(Context context) {
		return this.initCount > 0 && container.getContext() == context;
	}

	/* (non-Javadoc)
	 * @see com.socialize.SocializeService#isAuthenticated()
	 */
	@Override
	public boolean isAuthenticated() {
		return this.initCount > 0 && session != null;
	}

	/*
	 * (non-Javadoc)
	 * @see com.socialize.SocializeService#isAuthenticatedForRead(com.socialize.auth.AuthProviderType)
	 */
	@Override
	public boolean isAuthenticatedForRead(AuthProviderType providerType, String...permissions) {
		return isAuthenticated(providerType, true, permissions);
	}

	/*
	 * (non-Javadoc)
	 * @see com.socialize.SocializeService#isAuthenticatedForWrite(com.socialize.auth.AuthProviderType)
	 */
	@Override
	public boolean isAuthenticatedForWrite(AuthProviderType providerType, String...permissions) {
		return isAuthenticated(providerType, false, permissions);
	}

	private boolean isAuthenticated(AuthProviderType providerType, boolean readOnly, String...permissions) {
		if(isAuthenticated()) {

			if(providerType.equals(AuthProviderType.SOCIALIZE)) {
				return true;
			}

			UserProviderCredentials userProviderCredentials = session.getUserProviderCredentials(providerType);

			if(userProviderCredentials != null) {
				// Validate the credentials
				AuthProviderInfo authProviderInfo = userProviderCredentials.getAuthProviderInfo();

				if(authProviderInfo != null) {
					AuthProvider<AuthProviderInfo> provider = authProviders.getProvider(providerType);

					if(readOnly) {
						return provider.validateForRead(authProviderInfo, permissions);
					}
					else {
						return provider.validateForWrite(authProviderInfo, permissions);
					}
				}

				return false;
			}
		}

		return false;
	}

	/*
	 * (non-Javadoc)
	 * @see com.socialize.SocializeService#isAuthenticated(com.socialize.auth.AuthProviderType)
	 */
	@Deprecated
	@Override
	public boolean isAuthenticated(AuthProviderType providerType) {
		return isAuthenticated(providerType, false, FacebookFacade.DEFAULT_PERMISSIONS);
	}

	protected boolean assertAuthenticated(SocializeListener listener) {
		if(asserter != null) {
			return asserter.assertAuthenticated(this, session, listener);
		}

		if(session != null) {
			return true;
		}
		else {
			if(listener != null) {
				if(logger != null && logger.isInitialized()) {
					listener.onError(new SocializeException(logger.getMessage(SocializeLogger.NOT_AUTHENTICATED)));
				}
				else {
					listener.onError(new SocializeException("Not authenticated"));
				}
			}
			if(logger != null && logger.isInitialized()) {
				logger.error(SocializeLogger.NOT_AUTHENTICATED);
			}
			else {
				System.err.println("Not authenticated");
			}
		}

		return false;
	}

	protected boolean assertInitialized(Context context, SocializeListener listener) {
		if(asserter != null) {
			return asserter.assertInitialized(context, this, listener);
		}

		if(!isInitialized(context)) {
			if(listener != null) {
				if(logger != null && logger.isInitialized()) {
					listener.onError(new SocializeException(logger.getMessage(SocializeLogger.NOT_INITIALIZED)));
				}
				else {
					listener.onError(new SocializeException("Not initialized"));
				}
			}
			if(logger != null) {
				if(logger.isInitialized()) {
					logger.error(SocializeLogger.NOT_INITIALIZED);
				}
				else {
					logger.error("Socialize Not initialized!");
				}
			}

			return false;
		}
		return true;
	}

	/* (non-Javadoc)
	 * @see com.socialize.SocializeService#getSession()
	 */
	@Override
	public SocializeSession getSession() {
		return session;
	}

	/*
	 * (non-Javadoc)
	 * @see com.socialize.api.SocializeSessionConsumer#setSession(com.socialize.api.SocializeSession)
	 */
	@Override
	public void setSession(SocializeSession session) {
		this.session = session;
	}

	public void setLogger(SocializeLogger logger) {
		this.logger = logger;
	}

	public SocializeConfig getConfig() {
		return config;
	}

	public static class InitTask extends ManagedAsyncTask<Void, Void, IOCContainer> {
		private WeakReference<Context> context;
		private String[] paths;
		private Exception error;
		private SocializeInitListener listener;
		private SocializeServiceImpl service;
		private SocializeLogger logger;

		public InitTask(
				SocializeServiceImpl service,
				Context context,
				String[] paths,
				SocializeInitListener listener,
				SocializeLogger logger) {
			super();
			this.context = new WeakReference<Context>(context);
			this.paths = paths;
			this.listener = listener;
			this.service = service;
			this.logger = logger;
		}

		@Override
		public IOCContainer doInBackground(Void... params) {
			try {
				// Force null listener.  This will be called in postExecute
				return service.initWithContainer(context.get(), null, paths);
			}
			catch (Exception e) {
				error = e;
				return null;
			}
		}

		@Override
		public void onPostExecuteManaged(IOCContainer result) {
			if(result == null) {
				final String errorMessage = "Failed to initialize Socialize instance";

				if(listener != null) {
					if(error != null) {
						listener.onError(SocializeException.wrap(error));
					}
					else {
						listener.onError(new SocializeException(errorMessage));
					}
				}
				else {
					if(logger != null) {
						if(error != null) {
							logger.error(errorMessage, error);
						}
						else {
							logger.error(errorMessage);
						}
					}
					else {
						if(error != null) {
							SocializeLogger.e(errorMessage, error);
						}
						else {
							System.err.println(errorMessage);
						}
					}
				}
			}
			else {
				if(listener != null) {
					listener.onInit(context.get(), result);
				}
			}
		}
	};

	/**
	 * EXPERT ONLY (Not documented)
	 * @return
	 */
	IOCContainer getContainer() {
		return container;
	}

	@Override
	public boolean canShare(Context context, ShareType shareType) {
		return shareSystem.canShare(context, shareType);
	}

	/*
	 * (non-Javadoc)
	 * @see com.socialize.SocializeService#getEntityLoader()
	 */
	@Override
	public SocializeEntityLoader getEntityLoader() {
		return entityLoader;
	}

	/*
	 * (non-Javadoc)
	 * @see com.socialize.SocializeService#getSystem()
	 */
	@Override
	public SocializeSystem getSystem() {
		return system;
	}

	/*
	 * (non-Javadoc)
	 * @see com.socialize.SocializeService#setEntityLoader(com.socialize.ui.SocializeEntityLoader)
	 */
	@Override
	public void setEntityLoader(SocializeEntityLoader entityLoader) {
		this.entityLoader = entityLoader;
	}

	@Override
	public SocializeLogger getLogger() {
		return logger;
	}

	@Override
	public void onPause(Activity context) {
		paused = true;
		if(locationProvider != null) {
			locationProvider.pause(context);
		}
	}

	@Override
	public void onResume(final Activity context) {

		if(!Socialize.getSocialize().isInitialized(context)) {
			Socialize.getSocialize().initAsync(context, new SocializeInitListener() {
				@Override
				public void onError(SocializeException error) {
					SocializeLogger.e("Error occurred on resume", error);
				}

				@Override
				public void onInit(Context ctx, IOCContainer container) {
					// This is the current context
					setContext(ctx);

					if(appUtils != null) {
						appUtils.onResume(ctx);
					}

					if(FacebookUtils.isAvailable(ctx)) {
						try {
							FacebookUtils.onResume(context, null);
						}
						catch (Exception e) {
							SocializeLogger.e("Error occurred on resume", e);
						}
					}
				}
			});
		}
		else {
			if(paused && FacebookUtils.isAvailable(context)) {
				try {
					FacebookUtils.onResume(context, null);
				}
				catch (Exception e) {
					SocializeLogger.e("Error occurred on resume", e);
				}
				paused = false;
			}

			// This is the current context
			setContext(context);

			if(appUtils != null) {
				appUtils.onResume(context);
			}
		}
	}

	@Override
	public void onCreate(Activity context, Bundle savedInstanceState) {
        if(loopyService != null) {
            loopyService.onCreate(context);
        }
    }

	@Override
	public void onDestroy(Activity context) {
		onContextDestroyed(context);

        if(loopyService != null) {
            loopyService.onDestroy(context);
        }
	}

    @Override
    public void onStart(Activity context) {
        if(loopyService != null) {
            loopyService.onStart(context);
        }
    }

    @Override
    public void onStop(Activity context) {
        if(loopyService != null) {
            loopyService.onStop(context);
        }
    }

    protected void setShareSystem(ShareSystem shareSystem) {
		this.shareSystem = shareSystem;
	}

	protected void setUserSystem(UserSystem userSystem) {
		this.userSystem = userSystem;
	}

	protected void setAuthProviders(AuthProviders authProviders) {
		this.authProviders = authProviders;
	}

	protected void setAuthProviderInfoBuilder(AuthProviderInfoBuilder authProviderInfoBuilder) {
		this.authProviderInfoBuilder = authProviderInfoBuilder;
	}

	protected Intent newIntent(Activity context, Class<?> cls) {
		return new Intent(context, cls);
	}
}


/*
 * Copyright (c) 2012 Socialize Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.socialize.share;

import android.app.Activity;
import android.location.Location;
import com.sharethis.loopy.sdk.Item;
import com.sharethis.loopy.sdk.ShareCallback;
import com.sharethis.loopy.sdk.util.StringUtils;
import com.socialize.Socialize;
import com.socialize.SocializeService;
import com.socialize.api.action.ShareType;
import com.socialize.entity.DefaultPropagationInfo;
import com.socialize.entity.PropagationInfo;
import com.socialize.entity.PropagationInfoResponse;
import com.socialize.entity.SocializeAction;
import com.socialize.error.SocializeException;
import com.socialize.log.SocializeLogger;
import com.socialize.loopy.LoopyService;
import com.socialize.networks.SocialNetwork;
import com.socialize.networks.SocialNetworkListener;

/**
 * @author Jason Polites
 *
 */
public abstract class AbstractShareHandler implements ShareHandler {

    protected SocializeLogger logger;
    protected LoopyService loopyService;

	/* (non-Javadoc)
	 * @see com.socialize.share.ShareHandler#handle(android.app.Activity, com.socialize.entity.Share, com.socialize.share.ShareHandlerListener)
	 */
	@Override
	public void handle(final Activity context, final SocializeAction action, Location location, final String text, final SocialNetworkListener listener) {
        ShareType shareType = getShareType();
        final SocialNetwork network = SocialNetwork.valueOf(shareType);
		PropagationInfoResponse propagationInfoResponse = action.getPropagationInfoResponse();

		if(propagationInfoResponse != null) {
			final PropagationInfo propagationInfo = propagationInfoResponse.getPropagationInfo(shareType);

			if(propagationInfo != null) {
				try {
                    if(loopyService.isLoopyEnabled()) {

                        if(shareType.equals(ShareType.OTHER)) {
                            loopyService.showShareDialog(context, "", text, action.getEntity(), propagationInfo, listener);
                        } else {
                            loopyService.getTrackableUrl(action.getEntity(), shareType, propagationInfo.getEntityUrl(), new ShareCallback() {
                                @Override
                                public void onResult(Item item, Throwable error) {

                                    PropagationInfo infoInUse = loopyService.setShortlinks(propagationInfo, item, error);

                                    try {
                                        handle(context, action, text, infoInUse, listener);
                                    } catch (Exception e) {
                                        if(logger != null) {
                                            logger.error("Error handling share", e);
                                        }

                                        if(listener != null) {
                                            listener.onNetworkError(context, network, e);
                                        }
                                    }
                                }
                            });
                        }
                    } else {
                        handle(context, action, text, propagationInfo, listener);
                    }
				}
				catch (Exception e) {
					if(logger != null) {
						logger.error("Error handling share", e);
					}

					if(listener != null) {
						listener.onNetworkError(context, network, e);
					}
				}
			}
			else {
				logError(context, network, "No propagation info found for type [" +
                        shareType +
								"].  Share will not propagate", listener);
			}
		}
		else {
			logError(context, network, "No propagation info found for type [" +
                    shareType +
							"].  Share will not propagate", listener);
		}
	}

	protected void logError(Activity context, SocialNetwork network, String msg, SocialNetworkListener listener) {
		if(logger != null) {
			logger.warn(msg);
		}
		else {
			System.err.println(msg);
		}

		if(listener != null) {
			listener.onNetworkError(context, network, new SocializeException(msg));
		}
	}

	public void setLogger(SocializeLogger logger) {
		this.logger = logger;
	}

    @SuppressWarnings("unused")
    public void setLoopyService(LoopyService loopyService) {
        this.loopyService = loopyService;
    }

    protected abstract void handle(Activity context, SocializeAction action, String text, PropagationInfo info, SocialNetworkListener listener) throws Exception;

	protected abstract ShareType getShareType();

	protected SocializeService getSocialize() {
		return Socialize.getSocialize();
	}
}