package com.android.commands.monkey.ape.jev;

import java.text.Normalizer;
import java.security.MessageDigest;
import java.util.Iterator;
import java.util.ArrayDeque;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.LinkedHashSet;
import org.json.*;
import com.android.commands.monkey.ape.model.ModelAction;
import com.android.commands.monkey.ape.model.State;
import com.android.commands.monkey.ape.utils.StringCache;
import com.android.commands.monkey.ape.tree.GUITreeNode;
import com.android.commands.monkey.ape.tree.GUITree;

/** Values derived from APE's naming; no alternative state or action identity. */
public final class JevEvidence {
    public static final int MAX_TEXT_CODEPOINTS=512;
    private static final String TRUNCATED="…[truncated]";

    public static String boundedText(String text) { return boundedText(text,MAX_TEXT_CODEPOINTS); }
    public static String boundedText(String text,int limit) {
        if(text==null)return "";
        if(text.codePointCount(0,text.length())<=limit)return text;
        int prefix=limit-TRUNCATED.codePointCount(0,TRUNCATED.length());
        return text.substring(0,text.offsetByCodePoints(0,prefix))+TRUNCATED;
    }
    public static String boundedLabel(CharSequence text) {
        return boundedText(normalize(StringCache.removeQuotes(text)));
    }
    private JevEvidence() { }
    public static String normalize(String value) {
        if (value == null) return "";
        String n = Normalizer.normalize(value, Normalizer.Form.NFC);
        StringBuilder out = new StringBuilder();
        boolean space=false;
        for (int offset=0;offset<n.length();) {
            int cp=n.codePointAt(offset); offset+=Character.charCount(cp);
            if (Character.isWhitespace(cp) || Character.isSpaceChar(cp)) { space=out.length()>0; }
            else { if(space) out.append(' '); out.appendCodePoint(cp); space=false; }
        }
        return out.toString();
    }
    public static String digest(String value) {
        try {
            byte[] hash=MessageDigest.getInstance("SHA-256").digest(value.getBytes("UTF-8"));
            StringBuilder b=new StringBuilder();
            for(byte v:hash) b.append(String.format(java.util.Locale.ROOT,"%02x",v & 255));
            return b.toString();
        } catch(Exception e) { throw new IllegalStateException(e); }
    }
    public static JSONArray labels(GUITreeNode node) {
        JSONArray labels=new JSONArray();
        collect(node,0,labels); return labels;
    }
    private static void collect(GUITreeNode node,int depth,JSONArray labels) {
        if(node==null || depth>3 || labels.length()>=33) return;
        labels.put(new JSONArray().put(normalize(JevIdentity.text(node))).put(normalize(node.getContentDesc())));
        Iterator<GUITreeNode> it=node.getChildren();
        while(it.hasNext() && labels.length()<33) collect(it.next(),depth+1,labels);
    }
    private static boolean password(GUITreeNode node,int depth,int[] count) {
        if(node==null || depth>3 || count[0]>=33) return false;
        count[0]++;
        if(rawPassword(node)) return true;
        Iterator<GUITreeNode> it=node.getChildren();
        while(it.hasNext() && count[0]<33) if(password(it.next(),depth+1,count)) return true;
        return false;
    }
    public static JevRegistry.Member member(String type,GUITreeNode node) {
        boolean secret=password(node,0,new int[1]);
        for(GUITreeNode p=node;p!=null;p=p.getParent()) secret |= rawPassword(p);
        JSONArray roles=roleSignature(node),labels=fullLabels(node),extraLabels=new JSONArray();
        Set<String> seen=new HashSet<String>();
        for(int i=0;i<roles.length();i++) {
            JSONObject role=roles.optJSONObject(i);
            seen.add(new JSONArray().put(role.optString("text")).put(role.optString("content_desc")).toString());
        }
        seen.add(new JSONArray().put("").put("").toString());
        // Keep each label once; stateful descendant labels stay attached to their state.
        for(int i=0;i<labels.length();i++) {
            JSONArray pair=labels.optJSONArray(i);
            if(seen.add(pair.toString()))extraLabels.put(pair);
        }
        JSONObject local=probeLocalContext(node);
        if(local!=null && local.optJSONArray("labels").length()==0)local=null;
        return new JevRegistry.Member(type,extraLabels,roles,ConcreteActionIdentity.canonical(node),secret,local);
    }

    /** Detailed target, followed only by compact check/selection states of descendants. */
    public static JSONArray roleSignature(GUITreeNode node) {
        JSONArray out=new JSONArray();
        collectRole(node,0,out,new int[1]);
        return out;
    }
    private static void collectRole(GUITreeNode node,int depth,JSONArray out,int[] scanned) {
        if(node==null || depth>3 || out.length()>=8 || scanned[0]>=33)return;
        scanned[0]++;
        boolean checkable=node.isCheckable() || node.isChecked();
        if(depth==0 || checkable || Boolean.TRUE.equals(node.getJevSelected())) {
            try {
                JSONObject role=new JSONObject().put("class",boundedText(node.getClassName()))
                    .put("resource_id",boundedText(node.getResourceID()))
                    .put("text",rawText(node)).put("content_desc",rawContentDesc(node))
                    .put("selected",node.getJevSelected()==null?JSONObject.NULL:node.getJevSelected())
                    .put("enabled",node.isEnabled());
                if(depth==0) {
                    android.graphics.Rect b=node.getBoundsInScreen();
                    role.put("inputText",rawInputText(node)).put("password",rawPassword(node))
                        .put("index",node.getIndex()).put("clickable",node.isClickable())
                        .put("long_clickable",node.isLongClickable()).put("checkable",node.isCheckable())
                        .put("scrollable",node.isScrollable()).put("checked",node.isChecked())
                        .put("focusable",node.isFocusable()).put("focused",node.isFocused())
                        .put("bounds",b==null?JSONObject.NULL:new JSONArray().put(b.left).put(b.top).put(b.right).put(b.bottom));
                } else role.put("checked",checkable?node.isChecked():JSONObject.NULL);
                out.put(role);
            } catch(JSONException e) { throw new IllegalStateException(e); }
        }
        Iterator<GUITreeNode> it=node.getChildren();
        while(it.hasNext() && out.length()<8 && scanned[0]<33) collectRole(it.next(),depth+1,out,scanned);
    }
    static String rawText(GUITreeNode node) {
        if(node==null)return "";
        if(node.getJevText()!=null)return node.getJevText();
        return boundedLabel(node.getNodeInfo()==null?JevIdentity.text(node):node.getNodeInfo().getText());
    }
    private static String rawContentDesc(GUITreeNode node) {
        if(node==null)return "";
        if(node.getJevContentDesc()!=null)return node.getJevContentDesc();
        return boundedLabel(node.getNodeInfo()==null?node.getContentDesc():node.getNodeInfo().getContentDescription());
    }
    private static String rawInputText(GUITreeNode node) {
        return node==null?"":boundedText(node.getInputText());
    }
    private static boolean rawPassword(GUITreeNode node) {
        return node.getJevPassword()==null?node.isPassword():node.getJevPassword();
    }
    private static JSONArray fullLabels(GUITreeNode node) {
        JSONArray out=new JSONArray(); fullCollect(node,0,out); return out;
    }
    private static void fullCollect(GUITreeNode node,int depth,JSONArray out) {
        if(node==null || depth>3 || out.length()>=33)return;
        out.put(new JSONArray().put(rawText(node)).put(rawContentDesc(node)));
        Iterator<GUITreeNode> it=node.getChildren(); while(it.hasNext() && out.length()<33)fullCollect(it.next(),depth+1,out);
    }
    /** Bounded source-page observations; labels are clues, not inferred titles or modes. */
    public static JSONObject sourceContext(GUITree tree) {
        JSONArray labels=new JSONArray();ArrayDeque<GUITreeNode> pending=new ArrayDeque<GUITreeNode>();
        pending.add(tree.getRootNode());int scanned=0;boolean truncated=false;
        while(!pending.isEmpty() && scanned<128 && labels.length()<8) {
            GUITreeNode node=pending.removeFirst();scanned++;
            String text=rawText(node),desc=rawContentDesc(node);
            if(!text.isEmpty() || !desc.isEmpty())labels.put(JevRegistry.fields(
                "resource_id",boundedText(node.getResourceID(),128),"text",boundedText(text,128),
                "content_desc",boundedText(desc,128),"selected",node.getJevSelected()==null?JSONObject.NULL:node.getJevSelected()));
            Iterator<GUITreeNode> children=node.getChildren();
            while(children.hasNext()) {
                if(scanned+pending.size()>=128){truncated=true;break;}
                pending.addLast(children.next());
            }
        }
        return JevRegistry.fields("activity",boundedText(tree.getActivityClassName()),"page_labels",labels,
            "truncated",truncated || !pending.isEmpty());
    }
    /** Region's direct-parent evidence; paths express structure, not ownership. */
    static JSONObject localContext(GUITreeNode candidate) {
        return localContextAt(candidate,candidate==null?null:candidate.getParent());
    }

    /**
     * Source-side context captured before a concrete action is executed.  This
     * is diagnostic evidence only; it does not participate in matching or
     * action selection.  Keep the production matcher identity fields beside
     * the richer source/local observations so offline audits can join the
     * snapshot without treating context as a new identity predicate.
     */
    public static JSONObject contextSnapshot(final GUITree tree, final State state, final ModelAction action, final GUITreeNode node) {
        return com.android.commands.monkey.ape.naming.NameManager.withoutInterning(
            new java.util.function.Supplier<JSONObject>() {public JSONObject get(){return contextSnapshotReadOnly(tree,state,action,node);}});
    }
    private static JSONObject contextSnapshotReadOnly(GUITree tree, State state, ModelAction action, GUITreeNode node) {
        try {
            Object control = node == null ? JSONObject.NULL : controlSnapshot(node);
            JSONArray labels = node == null ? new JSONArray() : new JSONArray(member(action == null ? "" : action.getType().name(), node).labelsJson);
            JSONObject local = node == null ? new JSONObject().put("parent", JSONObject.NULL).put("labels", new JSONArray()).put("truncated", false) : localContext(node);
            JSONObject source = tree == null ? new JSONObject().put("activity", "").put("page_labels", new JSONArray()).put("truncated", false) : sourceContext(tree);
            String stateId = state == null ? "" : state.getGraphId();
            String actionId = action == null ? "" : action.getGraphId();
            String type = action == null ? "" : action.getType().name();
            String fullName = node == null ? "" : JevIdentity.fullName(node).toString();
            JSONObject matcher = new JSONObject().put("state_id", stateId).put("model_action_id", actionId)
                .put("action_type", type).put("full_name", fullName)
                .put("action_fingerprint", action == null ? "" : action.getActionFingerprint())
                .put("target_fingerprint", action == null ? "" : action.getTargetFingerprint())
                .put("target_descriptor", action == null ? "" : action.getTargetDescriptor());
            JSONObject result = new JSONObject().put("schema", 1).put("activity", tree == null ? "" : tree.getActivityClassName())
                .put("source_state_id", stateId).put("model_action_id", actionId).put("action_type", type)
                .put("full_name", fullName).put("control", control).put("target_subtree_labels", labels)
                .put("local_context", local).put("source_context", source).put("matcher", matcher);
            if(tree != null)result.put("gui_identity", guiIdentity(tree));
            return result;
        } catch(JSONException error) { throw new IllegalStateException(error); }
    }

    /** The concrete-node attributes used by the existing Region evidence. */
    public static JSONObject controlSnapshot(GUITreeNode node) {
        return JevRegistry.fields("class",boundedText(node.getClassName()),"resource_id",boundedText(node.getResourceID()),
            "text",rawText(node),"content_desc",boundedText(node.getContentDesc()),"enabled",node.isEnabled(),
            "clickable",node.isClickable(),"long_clickable",node.isLongClickable(),"scrollable",node.isScrollable(),
            "checked",node.isChecked(),"selected",node.getJevSelected()==null?JSONObject.NULL:node.getJevSelected());
    }
    /** Bounded source-node evidence, shared by current and confirmed historical actions. */
    static JSONObject pairAction(com.android.commands.monkey.ape.tree.GUITreeAction action,String sourcePage)throws JSONException {
        GUITreeNode node=action==null?null:action.getGUITreeNode();
        if(node==null || action.getGUITree()==null)return null;
        long structureStart=JevReviewProfile.start();
        JSONArray subtree=new JSONArray();boolean[] truncated={false};
        pairSubtree(node,0,new JSONArray(),subtree,truncated);
        JSONObject value=controlSnapshot(node).put("content_desc",rawContentDesc(node))
            .put("action_type",action.getActionType().name()).put("index",node.getIndex())
            .put("concrete_identity",ConcreteActionIdentity.of(action.getActionType().name(),node).toString())
            .put("model_action_id",action.getModelAction()==null?JSONObject.NULL:action.getModelAction().getGraphId())
            .put("source_state_id",action.getGUITree().getCurrentState()==null?JSONObject.NULL:action.getGUITree().getCurrentState().getGraphId())
            .put("bounds",rect(node.getBoundsInScreen())).put("source_page_id",sourcePage)
            .put("target_subtree",subtree).put("subtree_truncated",truncated[0])
            .put("parent",node.getParent()==null?JSONObject.NULL:controlSnapshot(node.getParent()))
            .put("local_context",localContext(node));
        JevReviewProfile.end("subtree_parent_local_context",structureStart);
        // Keep provenance explicit; never infer or restore missing labels.
        value.put("text_source",node.getJevText()!=null?"retained_gui_text":node.getNodeInfo()!=null?"source_node_info":"retained_native_node")
            .put("generated_image_text",node.isGeneratedImageText())
            .put("snapshot_provenance","retained source GUI node; no inferred or reconstructed labels");
        boolean labels=false;
        for(int i=0;i<subtree.length();i++)labels |= pairLabels(subtree.getJSONObject(i));
        if(node.getParent()!=null)labels |= pairLabels(value.getJSONObject("parent"));
        labels |= value.getJSONObject("local_context").getJSONArray("labels").length()>0;
        // A class/bounds-only anonymous control has no recoverable operation clues.
        if(!labels)return null;
        return value;
    }

    /**
     * Lightweight, read-only selection descriptor. It deliberately does not
     * allocate the prompt's subtree/parent/local-context JSON representation.
     * Its feature set mirrors JevHistoryPairwiseProtocol.selectionFeatures on
     * pairAction(), and recoverable uses the same label sources as pairLabels.
     */
    static final class PairDescriptor {
        final Set<String> selectionFeatures;
        final boolean recoverable;
        PairDescriptor(Set<String> features,boolean canRecover){
            selectionFeatures=Collections.unmodifiableSet(features);recoverable=canRecover;
        }
    }
    static PairDescriptor pairDescriptor(com.android.commands.monkey.ape.tree.GUITreeAction action) {
        GUITreeNode node=action==null?null:action.getGUITreeNode();
        if(node==null || action.getGUITree()==null)return new PairDescriptor(new LinkedHashSet<String>(),false);
        Set<String> features=new LinkedHashSet<String>();
        boolean[] recoverable={false};
        collectPairDescriptor(node,0,new int[]{0},action,features,recoverable);
        GUITreeNode parent=node.getParent();
        // Parent uses controlSnapshot's native (bounded) content-desc, whereas
        // the subtree uses the retained raw content-desc. Do not conflate them.
        if(parent!=null)recoverable[0] |= !rawText(parent).isEmpty()
            || !boundedText(parent.getContentDesc()).isEmpty() || !boundedText(parent.getResourceID()).isEmpty();
        // The pairAction contract also accepts visible sibling/local labels.
        // Defer this bounded scan until subtree and parent expose no label.
        if(!recoverable[0])try {recoverable[0]=localContext(node).getJSONArray("labels").length()>0;}
            catch(JSONException impossible){throw new IllegalStateException(impossible);}
        return new PairDescriptor(features,recoverable[0]);
    }
    private static void collectPairDescriptor(GUITreeNode node,int depth,int[] visited,
            com.android.commands.monkey.ape.tree.GUITreeAction action,Set<String> features,boolean[] recoverable) {
        JevDeadline.check();
        if(node==null || depth>3 || visited[0]>=32)return;
        visited[0]++;
        JevHistoryPairwiseProtocol.addSelectionFeature("text",rawText(node),features);
        JevHistoryPairwiseProtocol.addSelectionFeature("content_desc",rawContentDesc(node),features);
        JevHistoryPairwiseProtocol.addSelectionFeature("resource_id",boundedText(node.getResourceID()),features);
        JevHistoryPairwiseProtocol.addSelectionFeature("class",boundedText(node.getClassName()),features);
        if(node==action.getGUITreeNode())JevHistoryPairwiseProtocol.addSelectionFeature("action_type",action.getActionType().name(),features);
        recoverable[0] |= pairLabels(node);
        if(depth==3 || visited[0]>=32)return;
        Iterator<GUITreeNode> children=node.getChildren();
        while(children.hasNext() && visited[0]<32)collectPairDescriptor(children.next(),depth+1,visited,action,features,recoverable);
    }
    private static boolean pairLabels(JSONObject node) {
        return !node.optString("text").isEmpty() || !node.optString("content_desc").isEmpty() || !node.optString("resource_id").isEmpty();
    }
    private static boolean pairLabels(GUITreeNode node) {
        return node!=null && (!rawText(node).isEmpty() || !rawContentDesc(node).isEmpty() || !boundedText(node.getResourceID()).isEmpty());
    }
    private static JSONArray rect(android.graphics.Rect b) {return new JSONArray().put(b.left).put(b.top).put(b.right).put(b.bottom);}
    private static void pairSubtree(GUITreeNode node,int depth,JSONArray path,JSONArray rows,boolean[] truncated)throws JSONException {
        JevDeadline.check();
        if(rows.length()>=32){truncated[0]=true;return;}
        rows.put(controlSnapshot(node).put("content_desc",rawContentDesc(node)).put("path",path));
        if(depth==3){truncated[0] |= node.getChildCount()>0;return;}
        Iterator<GUITreeNode> children=node.getChildren();
        while(children.hasNext()) {
            if(rows.length()>=32){truncated[0]=true;break;}
            GUITreeNode child=children.next();JSONArray childPath=new JSONArray();
            for(int i=0;i<path.length();i++)childPath.put(path.getInt(i));childPath.put(child.getIndex());
            pairSubtree(child,depth+1,childPath,rows,truncated);
        }
    }

    /** Probe evidence may use the nearest ancestor (up to two levels) that actually exposes external labels. */
    private static JSONObject probeLocalContext(GUITreeNode candidate) {
        GUITreeNode parent=candidate==null?null:candidate.getParent();
        for(int hops=0;parent!=null && hops<2;hops++) {
            JSONObject context=localContextAt(candidate,parent);
            if(context.optJSONArray("labels").length()>0)return context;
            if(localBoundary(parent))break;
            parent=parent.getParent();
        }
        return localContextAt(candidate,candidate==null?null:candidate.getParent());
    }
    private static JSONObject localContextAt(GUITreeNode candidate,GUITreeNode parent) {
        JSONArray labels=new JSONArray();
        if(parent==null)return JevRegistry.fields("parent",JSONObject.NULL,"labels",labels,"truncated",false);
        String cls=parent.getClassName(),resource=parent.getResourceID();
        JSONObject container=JevRegistry.fields("class",boundedText(cls,128),"resource_id",boundedText(resource,128));
        boolean truncated=parent.getParent()==null || localBoundary(parent);
        if(!truncated)truncated=collectLocal(parent,candidate,new JSONArray(),labels,new int[1]);
        truncated |= !boundedText(cls,128).equals(cls==null?"":cls) || !boundedText(resource,128).equals(resource==null?"":resource);
        return JevRegistry.fields("parent",container,"labels",labels,"truncated",truncated);
    }
    private static boolean localBoundary(GUITreeNode node) {
        String cls=node.getClassName();
        return node.isScrollable() || cls.endsWith("ListView") || cls.endsWith("GridView")
            || cls.endsWith("RecyclerView") || cls.endsWith("ScrollView");
    }
    private static boolean collectLocal(GUITreeNode node,GUITreeNode candidate,JSONArray path,JSONArray labels,int[] scanned) {
        JevDeadline.check();
        if(node==candidate)return false;
        if(scanned[0]>=32 || labels.length()>=8)return true;
        scanned[0]++;
        if(Boolean.FALSE.equals(node.getJevVisibleToUser()))return false;
        String text=rawText(node),desc=rawContentDesc(node),shortText=boundedText(text,128),shortDesc=boundedText(desc,128);
        boolean truncated=!text.equals(shortText) || !desc.equals(shortDesc);
        if(!text.isEmpty() || !desc.isEmpty())labels.put(JevRegistry.fields("path",path,"text",shortText,"content_desc",shortDesc));
        if(path.length()==2 || path.length()>0 && localBoundary(node))return truncated || node.getChildCount()>0;
        Iterator<GUITreeNode> children=node.getChildren();
        while(children.hasNext()) {
            if(scanned[0]>=32 || labels.length()>=8)return true;
            GUITreeNode child=children.next();JSONArray childPath=new JSONArray();
            for(int i=0;i<path.length();i++)childPath.put(path.optInt(i));
            childPath.put(child.getIndex());
            truncated |= collectLocal(child,candidate,childPath,labels,scanned);
        }
        return truncated;
    }
    public static JSONArray resultLabels(GUITreeNode node) {
        JSONArray pairs=fullLabels(node), out=new JSONArray();
        try { for(int i=0;i<pairs.length();i++) {
            JSONArray p=pairs.getJSONArray(i);
            for(int j=0;j<2;j++) if(!p.getString(j).isEmpty()) out.put(p.getString(j));
        }} catch(JSONException e) {throw new IllegalStateException(e);}
        return out;
    }
    /** Complete collected GUI attributes, not the bounded prompt labels or abstract State ID. */
    static String guiIdentity(GUITree tree) {
        try {
            java.security.MessageDigest digest=java.security.MessageDigest.getInstance("SHA-256");
            ArrayDeque<GUITreeNode> pending=new ArrayDeque<GUITreeNode>();pending.add(tree.getRootNode());
            while(!pending.isEmpty()) {
                GUITreeNode n=pending.removeFirst();
                String row=new JSONArray().put(n.getChildCount()).put(n.getIndex()).put(n.getClassName()).put(n.getPackageName())
                    .put(n.getResourceID()).put(n.getText()).put(n.getContentDesc()).put(n.getInputText())
                    .put(n.getJevText()).put(n.getJevContentDesc()).put(n.getJevSelected()).put(n.getJevPassword())
                    .put(n.isEnabled()).put(n.isClickable()).put(n.isLongClickable()).put(n.isCheckable()).put(n.isChecked())
                    .put(n.isScrollable()).put(n.isFocusable()).put(n.isFocused()).put(n.isPassword())
                    .put(n.getJevVisibleToUser()).put(n.getJevDrawingOrder()).put(String.valueOf(n.getBoundsInScreen())).toString();
                digest.update(row.getBytes(JevProtocol.UTF8));digest.update((byte)'\n');
                Iterator<GUITreeNode> children=n.getChildren();while(children.hasNext())pending.addLast(children.next());
            }
            return java.util.Arrays.toString(digest.digest());
        } catch(java.security.NoSuchAlgorithmException e){throw new IllegalStateException(e);}
    }
    public static boolean resultUnsafe(GUITreeNode node) { return password(node,0,new int[1]); }

    /** A fresh read used only to reject replies after the actual window changed. */
    public static String accessibilityIdentity(android.view.accessibility.AccessibilityNodeInfo root) {
        if(root==null)return null;
        StringBuilder out=new StringBuilder();int[] count={0};
        try {if(!accessibilityIdentity(root,out,count))return null;return digest(out.toString());}
        catch(RuntimeException unavailable){return null;}
    }
    private static boolean accessibilityIdentity(android.view.accessibility.AccessibilityNodeInfo node,StringBuilder out,int[] count) {
        if(++count[0]>4096)return false;
        android.graphics.Rect bounds=new android.graphics.Rect();node.getBoundsInScreen(bounds);
        out.append(new JSONArray().put(String.valueOf(node.getClassName())).put(String.valueOf(node.getPackageName()))
            .put(node.getViewIdResourceName()).put(String.valueOf(node.getText())).put(String.valueOf(node.getContentDescription()))
            .put(node.isEnabled()).put(node.isClickable()).put(node.isLongClickable()).put(node.isScrollable())
            .put(node.isCheckable()).put(node.isChecked()).put(node.isSelected()).put(node.isFocusable()).put(node.isFocused())
            .put(node.isPassword()).put(node.isVisibleToUser()).put(bounds.toString()).put(node.getChildCount())).append('\n');
        for(int i=0;i<node.getChildCount();i++) {
            android.view.accessibility.AccessibilityNodeInfo child=node.getChild(i);
            if(child==null)return false;
            try {if(!accessibilityIdentity(child,out,count))return false;}finally{child.recycle();}
        }
        return true;
    }
}
