/*
 * Copyright 2017. nekocode ([email protected])
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package cn.nekocode.resinspector;

import android.app.Application;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.res.Resources;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RestrictTo;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import com.facebook.stetho.common.ProcessUtil;
import com.facebook.stetho.inspector.elements.DescriptorProvider;
import com.facebook.stetho.inspector.elements.Document;
import com.facebook.stetho.inspector.elements.DocumentProviderFactory;
import com.facebook.stetho.inspector.elements.android.ActivityTracker;
import com.facebook.stetho.inspector.elements.android.RIAndroidDocumentProviderFactory;
import com.facebook.stetho.inspector.protocol.ChromeDevtoolsDomain;
import com.facebook.stetho.inspector.protocol.module.CSS;
import com.facebook.stetho.inspector.protocol.module.DOM;
import com.facebook.stetho.inspector.protocol.module.Page;
import com.facebook.stetho.server.LazySocketHandler;
import com.facebook.stetho.server.LocalSocketServer;
import com.facebook.stetho.server.ProtocolDetectingSocketHandler;
import com.facebook.stetho.server.ServerManager;
import com.facebook.stetho.server.SocketHandler;
import com.facebook.stetho.server.SocketHandlerFactory;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;

/**
 * @author nekocode ([email protected])
 */
public class ResourceInspector {
    private static final String TAG = ResourceInspector.class.getSimpleName();
    private static final int TAG_RES_NAME = 0xF0F01358;
    private static final String DEVTOOLS_SUFFIX = "_devtools_remote";


    @NonNull
    public static Context wrap(@NonNull Context base) {
        return new InspectorContextWrapper(base);
    }

    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public static boolean isViewBeingInspected(@Nullable View view) {
        return view != null && view.getTag(ResourceInspector.TAG_RES_NAME) instanceof String;
    }

    @RestrictTo(RestrictTo.Scope.LIBRARY)
    @Nullable
    public static String getViewLayoutResName(@Nullable View view) {
        return view != null ? (String) view.getTag(ResourceInspector.TAG_RES_NAME) : null;
    }

    public static void initialize(@NonNull final Context context) {
        ActivityTracker.get().beginTrackingIfPossible((Application) context.getApplicationContext());

        final SocketHandler socketHandler = new LazySocketHandler(
                new SocketHandlerFactory() {
                    @Override
                    public SocketHandler create() {
                        final ProtocolDetectingSocketHandler socketHandler =
                                new ProtocolDetectingSocketHandler(context);

                        final Iterable<ChromeDevtoolsDomain> inspectorModules = getInspectorModules(context);
                        if (inspectorModules != null) {
                            socketHandler.addHandler(
                                    new ProtocolDetectingSocketHandler.AlwaysMatchMatcher(),
                                    new RIDevtoolsSocketHandler(context, inspectorModules));
                        }

                        return socketHandler;
                    }
                }
        );

        final String className = ResourceInspector.class.getSimpleName();
        final LocalSocketServer server = new LocalSocketServer(
                className, className + ProcessUtil.getProcessName() + DEVTOOLS_SUFFIX, socketHandler);

        final ServerManager serverManager = new ServerManager(server);
        serverManager.start();
    }

    private static Iterable<ChromeDevtoolsDomain> getInspectorModules(Context context) {
        final ArrayList<ChromeDevtoolsDomain> modules = new ArrayList<>();

        final DocumentProviderFactory factory = new RIAndroidDocumentProviderFactory(
                (Application) context.getApplicationContext(), Collections.<DescriptorProvider>emptyList());
        final Document document = new Document(factory);
        modules.add(new DOM(document));
        modules.add(new CSS(document));
        modules.add(new Page(context));

        return modules;
    }

    /*
      Internal Classes
     */

    private static class InspectorContextWrapper extends ContextWrapper {
        private InspectorLayoutInflater inflater;

        InspectorContextWrapper(Context base) {
            super(base);
        }

        @Override
        public Object getSystemService(String name) {
            if (LAYOUT_INFLATER_SERVICE.equals(name)) {
                if (inflater == null) {
                    inflater = new InspectorLayoutInflater(
                            (LayoutInflater) super.getSystemService(name), this);
                }
                return inflater;
            }
            return super.getSystemService(name);
        }
    }

    private static class InspectorLayoutInflater extends LayoutInflater {
        private final LayoutInflater original;
        private final String appPackageName;

        InspectorLayoutInflater(LayoutInflater original, Context newContext) {
            super(original, newContext);
            this.original = original;
            appPackageName = getContext().getApplicationContext().getPackageName();
        }

        @Override
        public LayoutInflater cloneInContext(Context newContext) {
            return new InspectorLayoutInflater(original.cloneInContext(newContext), newContext);
        }

        @Override
        public void setFactory(Factory factory) {
            super.setFactory(factory);
            original.setFactory(factory);
        }

        @Override
        public void setFactory2(Factory2 factory) {
            super.setFactory2(factory);
            original.setFactory2(factory);
            setPrivateFactoryInternal();
        }

        @Override
        public View inflate(int resourceId, ViewGroup root, boolean attachToRoot) {
            final Resources res = getContext().getResources();
            final String packageName = res.getResourcePackageName(resourceId);
            final String resName = res.getResourceEntryName(resourceId);
            final View view = original.inflate(resourceId, root, attachToRoot);

            if (!appPackageName.equals(packageName)) {
                return view;
            }

            View targetView = view;
            if (root != null && attachToRoot) {
                targetView = root.getChildAt(root.getChildCount() - 1);
            }

            targetView.setTag(TAG_RES_NAME, resName);

            return view;
        }

        private void setPrivateFactoryInternal() {
            // Skip if not attached to an activity.
            if (!(getContext() instanceof Factory2)) {
                return;
            }

            final Method setPrivateFactoryMethod = getMethod(LayoutInflater.class, "setPrivateFactory");
            if (setPrivateFactoryMethod != null) {
                invokeMethod(original, setPrivateFactoryMethod, (Factory2) getContext());
            }
        }

        static Method getMethod(Class clazz, String methodName) {
            final Method[] methods = clazz.getMethods();
            for (Method method : methods) {
                if (method.getName().equals(methodName)) {
                    method.setAccessible(true);
                    return method;
                }
            }
            return null;
        }

        static void invokeMethod(Object object, Method method, Object... args) {
            try {
                if (method == null) return;
                method.invoke(object, args);
            } catch (Exception e) {
                Log.d(TAG, "Can't invoke method using reflection", e);
            }
        }
    }
}