001package Torello.Browser;
002
003import Torello.Java.StringParse;
004
005/**
006 * Converts Chrome DevTools Protocol data objects and builders into readable diagnostic text.
007 *
008 * <BR /><BR />
009 * This class provides the shared implementation used by {@link BaseType#toString()} and by
010 * {@link AbstractBuilder#toString()}.  It walks the descriptor metadata associated with a CDP
011 * data-carrier object or builder, retrieves each field or assigned value, and prints the result
012 * using the compact type information defined by {@link CDPTypes}.
013 * 
014 * <BR /><BR />
015 * The implementation is intentionally split across three small internal-support classes:
016 *
017 * <BR /><BR />
018 * <UL CLASS=JDUL>
019 * 
020 * <LI><B><CODE>CDPToString</CODE></B> controls the overall formatting process.</LI>
021 * 
022 * <LI> <B><CODE>ParamRec</CODE></B> stores the current object, descriptor, indentation, padding,
023 *      and value-retrieval function for one formatting pass.
024 *      </LI>
025 * 
026 * <LI> <B><CODE>ValSwitch</CODE></B> performs the type-byte dispatch needed to print primitive
027 *      values, strings, arrays, raw JSON values, and nested CDP data types.
028 *      </LI>
029 * </UL>
030 *
031 * <BR /><BR />
032 * 💡 The important design point is that both {@code BaseType<?>} instances and
033 * {@code AbstractBuilder<?>} instances can be printed through the same descriptor-driven pathway.
034 * The only meaningful difference is how the value for each descriptor entry is retrieved.
035 */
036@Torello.JavaDoc.Annotations.JDHeaderBackgroundImg(EmbedTagFileID="INTERNAL_USE_JDHBI")
037public class CDPToString
038{
039    private CDPToString() { }
040
041
042    // ********************************************************************************************
043    // ********************************************************************************************
044    // Actual Entry Points
045    // ********************************************************************************************
046    // ********************************************************************************************
047
048
049    // Called from class 'AbstractBuilder'
050    static String toString(final AbstractBuilder<?> AB_THIS)
051    { return toString(new ParamRec(AB_THIS)); }
052
053    // Called from class 'BaseType'
054    static String toString(final BaseType<?> BT_THIS)
055    {
056        // Only Marker Events have "empty" classes.  Such events will never be nested inside of 
057        // another class.  This issue has already been checked in the surrounding code.
058
059        return (BT_THIS.numFields == 0)
060            ? BT_THIS.getClass().getName() + ": Marker-Event, w/ 0 Fields\n"
061            : toString(new ParamRec(BT_THIS));
062    }
063
064
065    // ********************************************************************************************
066    // ********************************************************************************************
067    // "Main" Method
068    // ********************************************************************************************
069    // ********************************************************************************************
070
071
072    // This is not private, because it is used inside 'ValSwitch'
073    // It is used to recursively print BaseType<?> and BaseType<?>[]
074    // Otherwise this would look much better if it were private.
075
076    static String toString(final ParamRec rec)
077    {
078        for (int i=0; i < rec.NUM; i++)
079        {
080            final Object value;
081
082            value = rec.getValue.apply(i);
083
084            rec.sb
085                .append(rec.INDENT)
086                .append(StringParse.rightSpacePad(rec.descriptor.names.get(i), rec.NAME_PADDING));
087
088            final byte typeByte = rec.descriptor.types.get(i);
089
090
091            // After 2 hours of arguing with Chat-GPT, it wrote this to describe what it just spent
092            // the afternoon doing.  Looks great if you ask me...
093            //
094            // IMPORTANT:
095            //
096            // Primitive/String/Number types can recover their printable type-name directly
097            // from the CDPTypes lookup tables.
098            //
099            // HOWEVER:
100            //
101            // Complex nested CDP types (CDP_TYPE / CDP_TYPE_ARRAY_1D) do not store their
102            // concrete Java class names inside the descriptor metadata itself.
103            //
104            // In those cases, the descriptor's package-private "getClass(...)" reflection
105            // method is used to recover the underlying Java type.
106            //
107            // This allows null-values to still print meaningful type information such as:
108            //
109            //     RunTime.PropertyPreview[]
110            //     Network.CookiePartitionKey
111            //
112            // rather than merely:
113            //
114            //     CDP_TYPE
115            //     CDP_TYPE_ARRAY_1D
116            //
117            // NOTE:
118            //
119            // CommandDescriptor reflection is intentionally limited to locating the actual
120            // Script-returning command method, NOT the zero-argument CommandBuilder factory
121            // method generated for convenience.
122
123            if (value != null)
124                ValSwitch.print(rec, value, typeByte);
125
126            else
127            {
128                final String cdpTypeStr = CDPTypes.types.get(typeByte);
129
130                final String typeStr = (cdpTypeStr != null)
131                    ? cdpTypeStr
132                    : className(rec.descriptor.getClass(rec.descriptor.names.get(i), i));
133
134                rec.sb.append(
135                    "null (" + typeStr + ", " +
136                    (rec.descriptor.optionals.get(i) ? "" : "not ") +
137                    "declared optional)\n"
138                );
139            }
140        }
141
142        final String I = StringParse.nChars(' ', rec.CURRENT_INDENT - 4);
143
144        return
145            rec.printedName + ":\n" +
146            I + "{\n" +
147            rec.sb.toString() +
148            I + "}\n";
149    }
150
151
152    // ********************************************************************************************
153    // ********************************************************************************************
154    // Static Helpers
155    // ********************************************************************************************
156    // ********************************************************************************************
157
158
159    // This helper intentionally preserves nested-class '.' notation.
160    //
161    // Java reflection APIs often emit nested types using '$':
162    // ➡️ RunTime$ObjectPreview
163    //
164    // whereas this utility converts the output into:
165    // ➡️ RunTime.ObjectPreview
166    //
167    // which is substantially more readable in generated diagnostics.
168    //
169    // Array types are recursively normalized so that:
170    // ➡️ RunTime.PropertyPreview[]
171    //
172    // is emitted instead of JVM-style array descriptors.
173
174    static String className(final Class<?> c)
175    {
176        if (c.isArray()) return className(c.getComponentType()) + "[]";
177
178        final String    canonical   = c.getCanonicalName();
179        final Package   pkg         = c.getPackage();
180
181        if (canonical == null)  throw new CDPToStringError("Canonical Name is null: " + c);
182        if (pkg == null)        throw new CDPToStringError("Package is null: " + canonical);
183
184        return canonical.substring(pkg.getName().length() + 1);
185    }
186
187    private static class CDPToStringError extends Error 
188    {
189        private static final long serialVersionUID = 1L;
190        CDPToStringError(final String s) { super(s); }
191    }
192
193}