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

package com.android.server.wm.flicker;

import android.annotation.Nullable;
import android.graphics.Rect;
import android.surfaceflinger.nano.Layers.LayerProto;
import android.surfaceflinger.nano.Layers.RectProto;
import android.surfaceflinger.nano.Layers.RegionProto;
import android.surfaceflinger.nano.Layerstrace.LayersTraceFileProto;
import android.surfaceflinger.nano.Layerstrace.LayersTraceProto;
import android.util.SparseArray;

import com.android.server.wm.flicker.Assertions.Result;

import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

/**
 * Contains a collection of parsed Layers trace entries and assertions to apply over
 * a single entry.
 *
 * Each entry is parsed into a list of {@link LayersTrace.Entry} objects.
 */
public class LayersTrace {
    final private List<Entry> mEntries;
    @Nullable
    final private Path mSource;

    private LayersTrace(List<Entry> entries, Path source) {
        this.mEntries = entries;
        this.mSource = source;
    }

    /**
     * Parses {@code LayersTraceFileProto} from {@code data} and uses the proto to generates a list
     * of trace entries, storing the flattened layers into its hierarchical structure.
     *
     * @param data   binary proto data
     * @param source Path to source of data for additional debug information
     */
    static LayersTrace parseFrom(byte[] data, Path source) {
        List<Entry> entries = new ArrayList<>();
        LayersTraceFileProto fileProto;
        try {
            fileProto = LayersTraceFileProto.parseFrom(data);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        for (LayersTraceProto traceProto : fileProto.entry) {
            Entry entry = Entry.fromFlattenedLayers(traceProto.elapsedRealtimeNanos,
                    traceProto.layers.layers);
            entries.add(entry);
        }
        return new LayersTrace(entries, source);
    }

    /**
     * Parses {@code LayersTraceFileProto} from {@code data} and uses the proto to generates a list
     * of trace entries, storing the flattened layers into its hierarchical structure.
     *
     * @param data binary proto data
     */
    static LayersTrace parseFrom(byte[] data) {
        return parseFrom(data, null);
    }

    List<Entry> getEntries() {
        return mEntries;
    }

    Entry getEntry(long timestamp) {
        Optional<Entry> entry = mEntries.stream()
                .filter(e -> e.getTimestamp() == timestamp)
                .findFirst();
        if (!entry.isPresent()) {
            throw new RuntimeException("Entry does not exist for timestamp " + timestamp);
        }
        return entry.get();
    }

    Optional<Path> getSource() {
        return Optional.ofNullable(mSource);
    }

    /**
     * Represents a single Layer trace entry.
     */
    static class Entry implements ITraceEntry {
        private long mTimestamp;
        private List<Layer> mRootLayers; // hierarchical representation of layers
        private List<Layer> mFlattenedLayers = null;

        private Entry(long timestamp, List<Layer> rootLayers) {
            this.mTimestamp = timestamp;
            this.mRootLayers = rootLayers;
        }

        /**
         * Constructs the layer hierarchy from a flattened list of layers.
         */
        static Entry fromFlattenedLayers(long timestamp, LayerProto[] protos) {
            SparseArray<Layer> layerMap = new SparseArray<>();
            ArrayList<Layer> orphans = new ArrayList<>();
            for (LayerProto proto : protos) {
                int id = proto.id;
                int parentId = proto.parent;

                Layer newLayer = layerMap.get(id);
                if (newLayer == null) {
                    newLayer = new Layer(proto);
                    layerMap.append(id, newLayer);
                } else if (newLayer.mProto != null) {
                    throw new RuntimeException("Duplicate layer id found:" + id);
                } else {
                    newLayer.mProto = proto;
                    orphans.remove(newLayer);
                }

                // add parent placeholder
                if (layerMap.get(parentId) == null) {
                    Layer orphanLayer = new Layer(null);
                    layerMap.append(parentId, orphanLayer);
                    orphans.add(orphanLayer);
                }
                layerMap.get(parentId).addChild(newLayer);
                newLayer.addParent(layerMap.get(parentId));
            }

            // Fail if we find orphan layers.
            orphans.remove(layerMap.get(-1));
            orphans.forEach(orphan -> {
                String childNodes = orphan.mChildren.stream().map(node ->
                        Integer.toString(node.getId())).collect(Collectors.joining(", "));
                int orphanId = orphan.mChildren.get(0).mProto.parent;
                throw new RuntimeException(
                        "Failed to parse layers trace. Found orphan layers with parent "
                                + "layer id:" + orphanId + " : " + childNodes);
            });

            return new Entry(timestamp, layerMap.get(-1).mChildren);
        }

        /**
         * Extracts {@link Rect} from {@link RectProto}.
         */
        private static Rect extract(RectProto proto) {
            return new Rect(proto.left, proto.top, proto.right, proto.bottom);
        }

        /**
         * Extracts {@link Rect} from {@link RegionProto} by returning a rect that encompasses all
         * the rects making up the region.
         */
        private static Rect extract(RegionProto regionProto) {
            Rect region = new Rect();
            for (RectProto proto : regionProto.rect) {
                region.union(proto.left, proto.top, proto.right, proto.bottom);
            }
            return region;
        }

        /**
         * Checks if a region specified by {@code testRect} is covered by all visible layers.
         */
        Result coversRegion(Rect testRect) {
            String assertionName = "coversRegion";
            Collection<Layer> layers = asFlattenedLayers();

            for (int x = testRect.left; x < testRect.right; x++) {
                for (int y = testRect.top; y < testRect.bottom; y++) {
                    boolean emptyRegionFound = true;
                    for (Layer layer : layers) {
                        if (layer.isInvisible() || layer.isHiddenByParent()) {
                            continue;
                        }
                        for (RectProto rectProto : layer.mProto.visibleRegion.rect) {
                            Rect r = extract(rectProto);
                            if (r.contains(x, y)) {
                                y = r.bottom;
                                emptyRegionFound = false;
                            }
                        }
                    }
                    if (emptyRegionFound) {
                        String reason = "Region to test: " + testRect
                                + "\nfirst empty point: " + x + ", " + y;
                        reason += "\nvisible regions:";
                        for (Layer layer : layers) {
                            if (layer.isInvisible() || layer.isHiddenByParent()) {
                                continue;
                            }
                            Rect r = extract(layer.mProto.visibleRegion);
                            reason += "\n" + layer.mProto.name + r.toString();
                        }
                        return new Result(false /* success */, this.mTimestamp, assertionName,
                                reason);
                    }
                }
            }
            String info = "Region covered: " + testRect;
            return new Result(true /* success */, this.mTimestamp, assertionName, info);
        }

        /**
         * Checks if a layer with name {@code layerName} has a visible region
         * {@code expectedVisibleRegion}.
         */
        Result hasVisibleRegion(String layerName, Rect expectedVisibleRegion) {
            String assertionName = "hasVisibleRegion";
            String reason = "Could not find " + layerName;
            for (Layer layer : asFlattenedLayers()) {
                if (layer.mProto.name.contains(layerName)) {
                    if (layer.isHiddenByParent()) {
                        reason = layer.getHiddenByParentReason();
                        continue;
                    }
                    if (layer.isInvisible()) {
                        reason = layer.getVisibilityReason();
                        continue;
                    }
                    Rect visibleRegion = extract(layer.mProto.visibleRegion);
                    if (visibleRegion.equals(expectedVisibleRegion)) {
                        return new Result(true /* success */, this.mTimestamp, assertionName,
                                layer.mProto.name + "has visible region " + expectedVisibleRegion);
                    }
                    reason = layer.mProto.name + " has visible region:" + visibleRegion + " "
                            + "expected:" + expectedVisibleRegion;
                }
            }
            return new Result(false /* success */, this.mTimestamp, assertionName, reason);
        }

        /**
         * Checks if a layer with name {@code layerName} is visible.
         */
        Result isVisible(String layerName) {
            String assertionName = "isVisible";
            String reason = "Could not find " + layerName;
            for (Layer layer : asFlattenedLayers()) {
                if (layer.mProto.name.contains(layerName)) {
                    if (layer.isHiddenByParent()) {
                        reason = layer.getHiddenByParentReason();
                        continue;
                    }
                    if (layer.isInvisible()) {
                        reason = layer.getVisibilityReason();
                        continue;
                    }
                    return new Result(true /* success */, this.mTimestamp, assertionName,
                            layer.mProto.name + " is visible");
                }
            }
            return new Result(false /* success */, this.mTimestamp, assertionName, reason);
        }

        @Override
        public long getTimestamp() {
            return mTimestamp;
        }

        List<Layer> getRootLayers() {
            return mRootLayers;
        }

        List<Layer> asFlattenedLayers() {
            if (mFlattenedLayers == null) {
                mFlattenedLayers = new ArrayList<>();
                ArrayList<Layer> pendingLayers = new ArrayList<>(this.mRootLayers);
                while (!pendingLayers.isEmpty()) {
                    Layer layer = pendingLayers.remove(0);
                    mFlattenedLayers.add(layer);
                    pendingLayers.addAll(layer.mChildren);
                }
            }
            return mFlattenedLayers;
        }

        Rect getVisibleBounds(String layerName) {
            List<Layer> layers = asFlattenedLayers();
            for (Layer layer : layers) {
                if (layer.mProto.name.contains(layerName) && layer.isVisible()) {
                    return extract(layer.mProto.visibleRegion);
                }
            }
            return new Rect(0, 0, 0, 0);
        }
    }

    /**
     * Represents a single layer with links to its parent and child layers.
     */
    static class Layer {
        @Nullable
        LayerProto mProto;
        List<Layer> mChildren;
        @Nullable
        Layer mParent = null;

        private Layer(LayerProto proto) {
            this.mProto = proto;
            this.mChildren = new ArrayList<>();
        }

        private void addChild(Layer childLayer) {
            this.mChildren.add(childLayer);
        }

        private void addParent(Layer parentLayer) {
            this.mParent = parentLayer;
        }

        int getId() {
            return mProto.id;
        }

        boolean isActiveBufferEmpty() {
            return this.mProto.activeBuffer == null || this.mProto.activeBuffer.height == 0
                    || this.mProto.activeBuffer.width == 0;
        }

        boolean isVisibleRegionEmpty() {
            if (this.mProto.visibleRegion == null) {
                return true;
            }
            Rect visibleRect = Entry.extract(this.mProto.visibleRegion);
            return visibleRect.height() == 0 || visibleRect.width() == 0;
        }

        boolean isHidden() {
            return (this.mProto.flags & /* FLAG_HIDDEN */ 0x1) != 0x0;
        }

        boolean isVisible() {
            return (!isActiveBufferEmpty() || isColorLayer()) &&
                    !isHidden() && this.mProto.color.a > 0 && !isVisibleRegionEmpty();
        }

        boolean isColorLayer() {
            return this.mProto.type.equals("ColorLayer");
        }

        boolean isRootLayer() {
            return mParent == null || mParent.mProto == null;
        }

        boolean isInvisible() {
            return !isVisible();
        }

        boolean isHiddenByParent() {
            return !isRootLayer() && (mParent.isHidden() || mParent.isHiddenByParent());
        }

        String getHiddenByParentReason() {
            String reason = "Layer " + mProto.name;
            if (isHiddenByParent()) {
                reason += " is hidden by parent: " + mParent.mProto.name;
            } else {
                reason += " is not hidden by parent: " + mParent.mProto.name;
            }
            return reason;
        }

        String getVisibilityReason() {
            String reason = "Layer " + mProto.name;
            if (isVisible()) {
                reason += " is visible:";
            } else {
                reason += " is invisible:";
                if (this.mProto.activeBuffer == null) {
                    reason += " activeBuffer=null";
                } else if (this.mProto.activeBuffer.height == 0) {
                    reason += " activeBuffer.height=0";
                } else if (this.mProto.activeBuffer.width == 0) {
                    reason += " activeBuffer.width=0";
                }
                if (!isColorLayer()) {
                    reason += " type != ColorLayer";
                }
                if (isHidden()) {
                    reason += " flags=" + this.mProto.flags + " (FLAG_HIDDEN set)";
                }
                if (this.mProto.color.a == 0) {
                    reason += " color.a=0";
                }
                if (isVisibleRegionEmpty()) {
                    reason += " visible region is empty";
                }
            }
            return reason;
        }
    }
}