package com.frozendevs.cache.cleaner.model;

import android.Manifest;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageDataObserver;
import android.content.pm.IPackageStatsObserver;
import android.content.pm.PackageManager;
import android.content.pm.PackageStats;
import android.os.AsyncTask;
import android.os.Binder;
import android.os.Build;
import android.os.Environment;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.StatFs;
import android.support.v4.content.ContextCompat;
import android.util.Log;
import android.widget.Toast;

import com.frozendevs.cache.cleaner.R;

import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;

public class CleanerService extends Service {

    public static final String ACTION_CLEAN_AND_EXIT = "com.frozendevs.cache.cleaner.CLEAN_AND_EXIT";

    private static final String TAG = "CleanerService";

    private Method mGetPackageSizeInfoMethod, mFreeStorageAndNotifyMethod;
    private OnActionListener mOnActionListener;
    private boolean mIsScanning = false;
    private boolean mIsCleaning = false;
    private long mCacheSize = 0;

    public interface OnActionListener {
        void onScanStarted(Context context);

        void onScanProgressUpdated(Context context, int current, int max);

        void onScanCompleted(Context context, List<AppsListItem> apps);

        void onCleanStarted(Context context);

        void onCleanCompleted(Context context, boolean succeeded);
    }

    public class CleanerServiceBinder extends Binder {

        public CleanerService getService() {
            return CleanerService.this;
        }
    }

    private CleanerServiceBinder mBinder = new CleanerServiceBinder();

    private class TaskScan extends AsyncTask<Void, Integer, List<AppsListItem>> {

        private int mAppCount = 0;

        @Override
        protected void onPreExecute() {
            if (mOnActionListener != null) {
                mOnActionListener.onScanStarted(CleanerService.this);
            }
        }

        @Override
        protected List<AppsListItem> doInBackground(Void... params) {
            mCacheSize = 0;

            final List<ApplicationInfo> packages = getPackageManager().getInstalledApplications(
                    PackageManager.GET_META_DATA);

            publishProgress(0, packages.size());

            final CountDownLatch countDownLatch = new CountDownLatch(packages.size());

            final List<AppsListItem> apps = new ArrayList<>();

            try {
                for (ApplicationInfo pkg : packages) {
                    mGetPackageSizeInfoMethod.invoke(getPackageManager(), pkg.packageName,
                            new IPackageStatsObserver.Stub() {

                                @Override
                                public void onGetStatsCompleted(PackageStats pStats,
                                                                boolean succeeded)
                                        throws RemoteException {
                                    synchronized (apps) {
                                        publishProgress(++mAppCount, packages.size());

                                        mCacheSize += addPackage(apps, pStats, succeeded);
                                    }

                                    synchronized (countDownLatch) {
                                        countDownLatch.countDown();
                                    }
                                }
                            }
                    );
                }

                countDownLatch.await();
            } catch (InvocationTargetException | InterruptedException | IllegalAccessException e) {
                e.printStackTrace();
            }

            return new ArrayList<>(apps);
        }

        @Override
        protected void onProgressUpdate(Integer... values) {
            if (mOnActionListener != null) {
                mOnActionListener.onScanProgressUpdated(CleanerService.this, values[0], values[1]);
            }
        }

        @Override
        protected void onPostExecute(List<AppsListItem> result) {
            if (mOnActionListener != null) {
                mOnActionListener.onScanCompleted(CleanerService.this, result);
            }

            mIsScanning = false;
        }

        private long addPackage(List<AppsListItem> apps, PackageStats pStats, boolean succeeded) {
            long cacheSize = 0;

            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
                cacheSize += pStats.cacheSize;
            }

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
                cacheSize += pStats.externalCacheSize;
            }

            if (!succeeded || cacheSize <= 0) {
                return 0;
            }

            try {
                PackageManager packageManager = getPackageManager();
                ApplicationInfo info = packageManager.getApplicationInfo(pStats.packageName,
                        PackageManager.GET_META_DATA);

                apps.add(new AppsListItem(pStats.packageName,
                        packageManager.getApplicationLabel(info).toString(),
                        packageManager.getApplicationIcon(pStats.packageName),
                        cacheSize));
            } catch (PackageManager.NameNotFoundException e) {
                e.printStackTrace();
            }

            return cacheSize;
        }
    }

    private class TaskClean extends AsyncTask<Void, Void, Boolean> {

        @Override
        protected void onPreExecute() {
            if (mOnActionListener != null) {
                mOnActionListener.onCleanStarted(CleanerService.this);
            }
        }

        @Override
        protected Boolean doInBackground(Void... params) {
            final CountDownLatch countDownLatch = new CountDownLatch(1);

            StatFs stat = new StatFs(Environment.getDataDirectory().getAbsolutePath());

            try {
                if (canCleanInternalCache(CleanerService.this)) {
                    mFreeStorageAndNotifyMethod.invoke(getPackageManager(),
                            (long) stat.getBlockCount() * (long) stat.getBlockSize(),
                            new IPackageDataObserver.Stub() {
                                @Override
                                public void onRemoveCompleted(String packageName, boolean succeeded)
                                        throws RemoteException {
                                    countDownLatch.countDown();
                                }
                            }
                    );
                } else {
                    countDownLatch.countDown();
                }

                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
                    if (isExternalStorageWritable()) {
                        final File externalDataDirectory = new File(Environment
                                .getExternalStorageDirectory().getAbsolutePath() + "/Android/data");

                        final String externalCachePath = externalDataDirectory.getAbsolutePath() +
                                "/%s/cache";

                        if (externalDataDirectory.isDirectory()) {
                            final File[] files = externalDataDirectory.listFiles();

                            for (File file : files) {
                                if (!deleteDirectory(new File(String.format(externalCachePath,
                                        file.getName())), true)) {
                                    Log.e(TAG, "External storage suddenly becomes unavailable");

                                    return false;
                                }
                            }
                        } else {
                            Log.e(TAG, "External data directory is not a directory!");
                        }
                    } else {
                        Log.d(TAG, "External storage is unavailable");
                    }
                }

                countDownLatch.await();
            } catch (InterruptedException | IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                return false;
            }

            return true;
        }

        @Override
        protected void onPostExecute(Boolean result) {
            if (result) {
                mCacheSize = 0;
            }

            if (mOnActionListener != null) {
                mOnActionListener.onCleanCompleted(CleanerService.this, result);
            }

            mIsCleaning = false;
        }

        private boolean deleteDirectory(File file, boolean directoryOnly) {
            if (!isExternalStorageWritable()) {
                return false;
            }

            if (file == null || !file.exists() || (directoryOnly && !file.isDirectory())) {
                return true;
            }

            if (file.isDirectory()) {
                final File[] children = file.listFiles();

                if (children != null) {
                    for (File child : children) {
                        if (!deleteDirectory(child, false)) {
                            return false;
                        }
                    }
                }
            }

            file.delete();

            return true;
        }

        private boolean isExternalStorageWritable() {
            return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }

    @Override
    public void onCreate() {
        try {
            mGetPackageSizeInfoMethod = getPackageManager().getClass().getMethod(
                    "getPackageSizeInfo", String.class, IPackageStatsObserver.class);

            mFreeStorageAndNotifyMethod = getPackageManager().getClass().getMethod(
                    "freeStorageAndNotify", long.class, IPackageDataObserver.class);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        String action = null;

        if (intent != null) {
            action = intent.getAction();
        }

        if (action == null) {
            return START_NOT_STICKY;
        }

        if (action.equals(ACTION_CLEAN_AND_EXIT)) {
            if (!canCleanExternalCache(this)) {
                Log.e(TAG, "Could not clean the cache: Insufficient permissions");

                Toast.makeText(this, getString(R.string.toast_could_not_clean_reason,
                        getString(R.string.rationale_title)), Toast.LENGTH_LONG).show();

                return START_NOT_STICKY;
            }

            setOnActionListener(new OnActionListener() {
                @Override
                public void onScanStarted(Context context) {
                }

                @Override
                public void onScanProgressUpdated(Context context, int current, int max) {
                }

                @Override
                public void onScanCompleted(Context context, List<AppsListItem> apps) {
                }

                @Override
                public void onCleanStarted(Context context) {
                }

                @Override
                public void onCleanCompleted(Context context, boolean succeeded) {
                    if (succeeded) {
                        Log.d(TAG, "Cache cleaned");
                    }
                    else {
                        Log.e(TAG, "Could not clean the cache");
                    }

                    Toast.makeText(CleanerService.this, succeeded ? R.string.cleaned :
                            R.string.toast_could_not_clean, Toast.LENGTH_LONG).show();

                    new Handler().postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            stopSelf();
                        }
                    }, 5000);
                }
            });

            cleanCache();
        }

        return START_NOT_STICKY;
    }

    public void scanCache() {
        mIsScanning = true;

        new TaskScan().execute();
    }

    public void cleanCache() {
        mIsCleaning = true;

        new TaskClean().execute();
    }

    public void setOnActionListener(OnActionListener listener) {
        mOnActionListener = listener;
    }

    public boolean isScanning() {
        return mIsScanning;
    }

    public boolean isCleaning() {
        return mIsCleaning;
    }

    public long getCacheSize() {
        return mCacheSize;
    }

    private static boolean hasPermission(Context context, String permission) {
        return ContextCompat.checkSelfPermission(context, permission) ==
                PackageManager.PERMISSION_GRANTED;
    }

    public static boolean canCleanInternalCache(Context context) {
        return hasPermission(context, Manifest.permission.CLEAR_APP_CACHE);
    }

    public static boolean canCleanExternalCache(Context context) {
        return hasPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE);
    }
}