/*
 * Copyright (c) 2020, 2022, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * The Universal Permissive License (UPL), Version 1.0
 *
 * Subject to the condition set forth below, permission is hereby granted to any
 * person obtaining a copy of this software, associated documentation and/or
 * data (collectively the "Software"), free of charge and under any and all
 * copyright rights in the Software, and any and all patent rights owned or
 * freely licensable by each licensor hereunder covering either (i) the
 * unmodified Software as contributed to or provided by such licensor, or (ii)
 * the Larger Works (as defined below), to deal in both
 *
 * (a) the Software, and
 *
 * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
 * one is included with the Software each a "Larger Work" to which the Software
 * is contributed by such licensors),
 *
 * without restriction, including without limitation the rights to copy, create
 * derivative works of, display, perform, and distribute the Software and make,
 * use, sell, offer for sale, import, export, have made, and have sold the
 * Software and the Larger Work(s), and to sublicense the foregoing rights on
 * either these or other terms.
 *
 * This license is subject to the following condition:
 *
 * The above copyright notice and either this complete permission notice or at a
 * minimum a reference to the UPL must be included in all copies or substantial
 * portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package com.oracle.graal.python.builtins.modules;

import static com.oracle.graal.python.builtins.PythonBuiltinClassType.LookupError;
import static com.oracle.graal.python.builtins.modules.io.IONodes.T_NAME;
import static com.oracle.graal.python.nodes.BuiltinNames.J_ENCODE;
import static com.oracle.graal.python.nodes.BuiltinNames.J__CODECS_TRUFFLE;
import static com.oracle.graal.python.nodes.BuiltinNames.T_ENCODE;
import static com.oracle.graal.python.nodes.BuiltinNames.T__CODECS_TRUFFLE;
import static com.oracle.graal.python.nodes.ErrorMessages.IS_NOT_TEXT_ENCODING;
import static com.oracle.graal.python.nodes.SpecialAttributeNames.T___MODULE__;
import static com.oracle.graal.python.nodes.SpecialAttributeNames.T___QUALNAME__;
import static com.oracle.graal.python.nodes.SpecialMethodNames.J_DECODE;
import static com.oracle.graal.python.nodes.SpecialMethodNames.J___CALL__;
import static com.oracle.graal.python.nodes.SpecialMethodNames.J___INIT__;
import static com.oracle.graal.python.nodes.SpecialMethodNames.T_DECODE;
import static com.oracle.graal.python.nodes.SpecialMethodNames.T___INIT__;
import static com.oracle.graal.python.util.PythonUtils.tsLiteral;

import java.util.ArrayList;
import java.util.List;

import com.oracle.graal.python.PythonLanguage;
import com.oracle.graal.python.builtins.Builtin;
import com.oracle.graal.python.builtins.CoreFunctions;
import com.oracle.graal.python.builtins.PythonBuiltinClassType;
import com.oracle.graal.python.builtins.PythonBuiltins;
import com.oracle.graal.python.builtins.modules.CodecsModuleBuiltins.CodecsDecodeNode;
import com.oracle.graal.python.builtins.modules.CodecsModuleBuiltins.CodecsEncodeNode;
import com.oracle.graal.python.builtins.modules.CodecsTruffleModuleBuiltinsFactory.CallApplyNodeGen;
import com.oracle.graal.python.builtins.modules.CodecsTruffleModuleBuiltinsFactory.CodecDecodeNodeGen;
import com.oracle.graal.python.builtins.modules.CodecsTruffleModuleBuiltinsFactory.CodecInitNodeGen;
import com.oracle.graal.python.builtins.modules.CodecsTruffleModuleBuiltinsFactory.EncodeNodeGen;
import com.oracle.graal.python.builtins.modules.CodecsTruffleModuleBuiltinsFactory.IncrementalDecodeNodeGen;
import com.oracle.graal.python.builtins.modules.CodecsTruffleModuleBuiltinsFactory.IncrementalEncodeNodeGen;
import com.oracle.graal.python.builtins.modules.CodecsTruffleModuleBuiltinsFactory.StreamDecodeNodeGen;
import com.oracle.graal.python.builtins.objects.PNone;
import com.oracle.graal.python.builtins.objects.function.PKeyword;
import com.oracle.graal.python.builtins.objects.module.PythonModule;
import com.oracle.graal.python.builtins.objects.object.PythonObject;
import com.oracle.graal.python.builtins.objects.tuple.PTuple;
import com.oracle.graal.python.builtins.objects.tuple.TupleBuiltins;
import com.oracle.graal.python.builtins.objects.type.PythonAbstractClass;
import com.oracle.graal.python.builtins.objects.type.PythonClass;
import com.oracle.graal.python.builtins.objects.type.TypeNodes.GetSuperClassNode;
import com.oracle.graal.python.lib.PyObjectCallMethodObjArgs;
import com.oracle.graal.python.lib.PyObjectGetAttr;
import com.oracle.graal.python.lib.PyObjectStrAsTruffleStringNode;
import com.oracle.graal.python.nodes.PGuards;
import com.oracle.graal.python.nodes.PNodeWithContext;
import com.oracle.graal.python.nodes.PRaiseNode;
import com.oracle.graal.python.nodes.attributes.SetAttributeNode;
import com.oracle.graal.python.nodes.call.CallNode;
import com.oracle.graal.python.nodes.call.special.CallVarargsMethodNode;
import com.oracle.graal.python.nodes.call.special.LookupAndCallVarargsNode;
import com.oracle.graal.python.nodes.function.PythonBuiltinBaseNode;
import com.oracle.graal.python.nodes.function.builtins.PythonQuaternaryBuiltinNode;
import com.oracle.graal.python.nodes.function.builtins.PythonTernaryBuiltinNode;
import com.oracle.graal.python.nodes.function.builtins.PythonVarargsBuiltinNode;
import com.oracle.graal.python.nodes.object.GetClassNode;
import com.oracle.graal.python.nodes.statement.AbstractImportNode;
import com.oracle.graal.python.runtime.PythonContext;
import com.oracle.graal.python.runtime.object.PythonObjectFactory;
import com.oracle.graal.python.util.PythonUtils;
import com.oracle.graal.python.util.Supplier;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.dsl.Cached;
import com.oracle.truffle.api.dsl.Cached.Shared;
import com.oracle.truffle.api.dsl.GenerateUncached;
import com.oracle.truffle.api.dsl.ImportStatic;
import com.oracle.truffle.api.dsl.NodeFactory;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.frame.Frame;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.strings.TruffleString;

@CoreFunctions(defineModule = J__CODECS_TRUFFLE)
public class CodecsTruffleModuleBuiltins extends PythonBuiltins {

    private static final TruffleString T_CODEC_INFO_NAME = tsLiteral("CodecInfo");
    private static final TruffleString T_CODEC = tsLiteral("Codec");
    private static final TruffleString T_INCREMENTAL_ENCODER = tsLiteral("IncrementalEncoder");
    private static final TruffleString T_BUFFERED_INCREMENTAL_DECODER = tsLiteral("BufferedIncrementalDecoder");
    private static final TruffleString T_STREAM_READER = tsLiteral("StreamReader");
    private static final TruffleString T_STREAM_WRITER = tsLiteral("StreamWriter");

    private static final String J_BUFFER_DECODE = "_buffer_decode";

    private static final TruffleString T_TRUFFLE_CODEC = tsLiteral("TruffleCodec");
    private static final TruffleString T_TRUFFLE_INCREMENTAL_ENCODER = tsLiteral("TruffleIncrementalEncoder");
    private static final TruffleString T_TRUFFLE_INCREMENTAL_DECODER = tsLiteral("TruffleIncrementalDecoder");
    private static final TruffleString T_TRUFFLE_STREAM_WRITER = tsLiteral("TruffleStreamWriter");
    private static final TruffleString T_TRUFFLE_STREAM_READER = tsLiteral("TruffleStreamReader");
    private static final TruffleString T_APPLY_ENCODING = tsLiteral("ApplyEncoding");

    private static final TruffleString T_ATTR_ENCODING = tsLiteral("encoding");
    private static final TruffleString T_ATTR_ERRORS = tsLiteral("errors");
    private static final TruffleString T_ATTR_FN = tsLiteral("fn");
    private static final TruffleString T_INCREMENTALENCODER = tsLiteral("incrementalencoder");
    private static final TruffleString T_INCREMENTALDECODER = tsLiteral("incrementaldecoder");
    private static final TruffleString T_STREAMREADER = tsLiteral("streamreader");
    private static final TruffleString T_STREAMWRITER = tsLiteral("streamwriter");
    private static final TruffleString T_CODECS = tsLiteral("codecs");

    private PythonClass truffleCodecClass;
    private PythonClass truffleIncrementalEncoderClass;
    private PythonClass truffleIncrementalDecoderClass;
    private PythonClass truffleStreamReaderClass;
    private PythonClass truffleStreamWriterClass;
    private PythonClass applyEncodingClass;

    @Override
    protected List<? extends NodeFactory<? extends PythonBuiltinBaseNode>> getNodeFactories() {
        return new ArrayList<>();
    }

    private static PythonClass initClass(TruffleString className, TruffleString superClassName, BuiltinDescr[] descrs, PythonModule codecsTruffleModule, PythonModule codecsModule,
                    PythonLanguage language,
                    PythonObjectFactory factory) {
        PythonAbstractClass superClass = (PythonAbstractClass) codecsModule.getAttribute(superClassName);
        return initClass(className, superClass, descrs, codecsTruffleModule, language, factory);
    }

    private static PythonClass initClass(TruffleString className, PythonAbstractClass superClass, BuiltinDescr[] descrs, PythonModule codecsTruffleModule, PythonLanguage language,
                    PythonObjectFactory factory) {
        PythonClass clazz = factory.createPythonClassAndFixupSlots(language, PythonBuiltinClassType.PythonClass, className, new PythonAbstractClass[]{superClass});
        for (BuiltinDescr d : descrs) {
            PythonUtils.createMethod(language, clazz, d.nodeClass, d.enclosingType ? clazz : null, 1, d.nodeSupplier, factory);
        }
        clazz.setAttribute(T___MODULE__, T__CODECS_TRUFFLE);
        clazz.setAttribute(T___QUALNAME__, T__CODECS_TRUFFLE);
        codecsTruffleModule.setAttribute(className, clazz);
        return clazz;
    }

    private static final class BuiltinDescr {
        final Supplier<PythonBuiltinBaseNode> nodeSupplier;
        final Class<?> nodeClass;
        final boolean enclosingType;

        public BuiltinDescr(Supplier<PythonBuiltinBaseNode> nodeSupplier, Class<?> nodeClass, boolean enclosingType) {
            this.nodeSupplier = nodeSupplier;
            this.nodeClass = nodeClass;
            this.enclosingType = enclosingType;
        }
    }

    @TruffleBoundary
    static PTuple codecsInfo(PythonModule self, TruffleString encoding, PythonContext context, PythonObjectFactory factory) {
        PythonModule codecsModule = (PythonModule) AbstractImportNode.importModule(T_CODECS);
        CodecsTruffleModuleBuiltins codecsTruffleBuiltins = (CodecsTruffleModuleBuiltins) self.getBuiltins();
        if (self.getAttribute(T_TRUFFLE_CODEC) instanceof PNone) {
            initCodecClasses(self, codecsModule, context, factory);
        }

        // encode/decode methods for codecs.CodecInfo
        PythonObject truffleCodec = factory.createPythonObject(codecsTruffleBuiltins.truffleCodecClass);
        truffleCodec.setAttribute(T_ATTR_ENCODING, encoding);
        Object encodeMethod = PyObjectGetAttr.getUncached().execute(null, truffleCodec, T_ENCODE);
        Object decodeMethod = PyObjectGetAttr.getUncached().execute(null, truffleCodec, T_DECODE);

        // incrementalencoder factory function for codecs.CodecInfo
        PythonObject tie = factory.createPythonObject(codecsTruffleBuiltins.applyEncodingClass);
        tie.setAttribute(T_ATTR_FN, codecsTruffleBuiltins.truffleIncrementalEncoderClass);
        tie.setAttribute(T_ATTR_ENCODING, encoding);

        // incrementaldecoder factory function for codecs.CodecInfo
        PythonObject tid = factory.createPythonObject(codecsTruffleBuiltins.applyEncodingClass);
        tid.setAttribute(T_ATTR_FN, codecsTruffleBuiltins.truffleIncrementalDecoderClass);
        tid.setAttribute(T_ATTR_ENCODING, encoding);

        // streamwriter factory function for codecs.CodecInfo
        PythonObject sr = factory.createPythonObject(codecsTruffleBuiltins.applyEncodingClass);
        sr.setAttribute(T_ATTR_FN, codecsTruffleBuiltins.truffleStreamReaderClass);
        sr.setAttribute(T_ATTR_ENCODING, encoding);

        // streamreader factory function for codecs.CodecInfo
        PythonObject sw = factory.createPythonObject(codecsTruffleBuiltins.applyEncodingClass);
        sw.setAttribute(T_ATTR_FN, codecsTruffleBuiltins.truffleStreamWriterClass);
        sw.setAttribute(T_ATTR_ENCODING, encoding);

        // codecs.CodecInfo
        PythonAbstractClass codecInfoClass = (PythonAbstractClass) codecsModule.getAttribute(T_CODEC_INFO_NAME);
        return (PTuple) CallVarargsMethodNode.getUncached().execute(null, codecInfoClass, new Object[]{}, createCodecInfoArgs(encoding, encodeMethod, decodeMethod, tie, tid, sr, sw));
    }

    private static PKeyword[] createCodecInfoArgs(TruffleString encoding, Object encodeMethod, Object decodeMethod, PythonObject tie, PythonObject tid, PythonObject sr, PythonObject sw) {
        return new PKeyword[]{
                        new PKeyword(T_NAME, encoding),
                        new PKeyword(T_ENCODE, encodeMethod),
                        new PKeyword(T_DECODE, decodeMethod),
                        new PKeyword(T_INCREMENTALENCODER, tie),
                        new PKeyword(T_INCREMENTALDECODER, tid),
                        new PKeyword(T_STREAMREADER, sr),
                        new PKeyword(T_STREAMWRITER, sw)
        };
    }

    protected SetAttributeNode createSetEncoding() {
        return SetAttributeNode.create(T_ATTR_ENCODING);
    }

    /**
     * create classes based on types declared in lib/3/codes.py
     */
    // @formatter:off
    private static void initCodecClasses(PythonModule codecsTruffleModule, PythonModule codecsModule, PythonContext context, PythonObjectFactory factory) {

        // TODO - the incremental codec and reader/writer won't work well with stateful
        // encodings, like some of the CJK encodings
        CodecsTruffleModuleBuiltins codecsTruffleBuiltins = (CodecsTruffleModuleBuiltins) codecsTruffleModule.getBuiltins();
        PythonLanguage language = PythonLanguage.get(null);

        // class TruffleCodec(codecs.Codec):
        //     def encode(self, input, errors='strict'):
        //         return _codecs.__truffle_encode__(input, self.encoding, errors)
        //     def decode(self, input, errors='strict'):
        //         return _codecs.__truffle_decode__(input, self.encoding, errors, True)
        codecsTruffleBuiltins.truffleCodecClass = initClass(T_TRUFFLE_CODEC, (PythonClass) codecsModule.getAttribute(T_CODEC),
                        new BuiltinDescr[]{
                                        new BuiltinDescr(() -> EncodeNodeGen.create(), EncodeNode.class, false),
                                        new BuiltinDescr(() -> CodecDecodeNodeGen.create(), CodecDecodeNode.class, true)},
                        codecsTruffleModule, language, factory);

        // class TruffleIncrementalEncoder(codecs.IncrementalEncoder):
        //     def __init__(self, encoding, *args, **kwargs):
        //         super().__init__(*args, **kwargs)
        //         self.encoding = encoding
        //     def encode(self, input, final=False):
        //         return _codecs.__truffle_encode__(input, self.encoding, self.errors)[0]
        codecsTruffleBuiltins.truffleIncrementalEncoderClass = initClass(T_TRUFFLE_INCREMENTAL_ENCODER, T_INCREMENTAL_ENCODER,
                        new BuiltinDescr[]{
                                        new BuiltinDescr(() -> CodecInitNodeGen.create(), CodecInitNode.class, false),
                                        new BuiltinDescr(() -> IncrementalEncodeNodeGen.create(), IncrementalEncodeNode.class, true)},
                        codecsTruffleModule, codecsModule, language, factory);

        // class TruffleIncrementalDecoder(codecs.BufferedIncrementalDecoder):
        //     def __init__(self, encoding, *args, **kwargs):
        //         super().__init__(*args, **kwargs)
        //         self.encoding = encoding
        //     def _buffer_decode(self, input, errors, final):
        //         return _codecs.__truffle_decode__(input, self.encoding, errors, final)
        codecsTruffleBuiltins.truffleIncrementalDecoderClass = initClass(T_TRUFFLE_INCREMENTAL_DECODER, T_BUFFERED_INCREMENTAL_DECODER,
                        new BuiltinDescr[]{
                                        new BuiltinDescr(() -> CodecInitNodeGen.create(), CodecInitNode.class, false),
                                        new BuiltinDescr(() -> IncrementalDecodeNodeGen.create(), IncrementalDecodeNode.class, true)},
                        codecsTruffleModule, codecsModule, language, factory);

        // class TruffleStreamWriter(codecs.StreamWriter):
        //     def __init__(self, encoding, *args, **kwargs):
        //         super().__init__(*args, **kwargs)
        //         self.encoding = encoding
        //     def encode(self, input, errors='strict'):
        //         return _codecs.__truffle_encode__(input, self.encoding, errors)
        codecsTruffleBuiltins.truffleStreamWriterClass = initClass(T_TRUFFLE_STREAM_WRITER, T_STREAM_WRITER,
                        new BuiltinDescr[]{
                                        new BuiltinDescr(() -> CodecInitNodeGen.create(), CodecInitNode.class, false),
                                        new BuiltinDescr(() -> EncodeNodeGen.create(), EncodeNode.class, true)},
                        codecsTruffleModule, codecsModule, language, factory);

        // class TruffleStreamReader(codecs.StreamReader):
        //     def __init__(self, encoding, *args, **kwargs):
        //         super().__init__(*args, **kwargs)
        //         self.encoding = encoding
        //     def decode(self, input, errors='strict'):
        //         return _codecs.__truffle_decode__(input, self.encoding, errors)
        codecsTruffleBuiltins.truffleStreamReaderClass = initClass(T_TRUFFLE_STREAM_READER, T_STREAM_READER,
                        new BuiltinDescr[]{
                                        new BuiltinDescr(() -> CodecInitNodeGen.create(), CodecInitNode.class, false),
                                        new BuiltinDescr(() -> StreamDecodeNodeGen.create(), StreamDecodeNode.class, true)},
                        codecsTruffleModule, codecsModule, language, factory);

        // serves as factory function for CodecInfo-s incrementalencoder/decode and streamwriter/reader
        // class apply_encoding:
        //     def __call__(self, *args, **kwargs):
        //         return self.fn(self.encoding, *args, **kwargs)
        codecsTruffleBuiltins.applyEncodingClass = initClass(T_APPLY_ENCODING, context.lookupType(PythonBuiltinClassType.PythonObject),
                        new BuiltinDescr[]{new BuiltinDescr(() -> CallApplyNodeGen.create(), CallApplyNode.class, false)},
                        codecsTruffleModule, language, factory);
    }
    // @formatter:on

    @Builtin(name = J___INIT__, minNumOfPositionalArgs = 1, takesVarArgs = true, takesVarKeywordArgs = true)
    protected abstract static class CodecInitNode extends PythonVarargsBuiltinNode {
        @Specialization
        Object init(VirtualFrame frame, PythonObject self, Object[] args, PKeyword[] kw,
                        @Cached PyObjectGetAttr getAttrNode,
                        @Cached("createSetAttr()") SetAttributeNode setAttrNode,
                        @Cached GetClassNode getClass,
                        @Cached GetSuperClassNode getSuperClassNode,
                        @Cached CallNode callNode) {
            assert args.length > 0;
            Object superClass = getSuperClassNode.execute(getClass.execute(self));
            Object superInit = getAttrNode.execute(frame, superClass, T___INIT__);
            Object[] callArgs = new Object[args.length];
            callArgs[0] = self;
            if (args.length > 1) {
                PythonUtils.arraycopy(args, 1, callArgs, 1, args.length - 1);
            }
            callNode.execute(frame, superInit, callArgs, kw);
            setAttrNode.executeVoid(frame, self, args[0]);
            return PNone.NONE;
        }

        protected LookupAndCallVarargsNode createCallNode() {
            return LookupAndCallVarargsNode.create(T___INIT__);
        }

        protected SetAttributeNode createSetAttr() {
            return SetAttributeNode.create(T_ATTR_ENCODING);
        }
    }

    @Builtin(name = J___CALL__, minNumOfPositionalArgs = 1, takesVarArgs = true, takesVarKeywordArgs = true)
    protected abstract static class CallApplyNode extends PythonVarargsBuiltinNode {
        @Specialization
        Object call(VirtualFrame frame, PythonObject self, Object[] args, PKeyword[] kw,
                        @Cached PyObjectGetAttr getAttrNode,
                        @Cached CallVarargsMethodNode callNode) {
            Object[] callArgs = new Object[args.length + 1];
            callArgs[0] = getAttrNode.execute(frame, self, T_ATTR_ENCODING);
            PythonUtils.arraycopy(args, 0, callArgs, 1, args.length);
            return callNode.execute(frame, getAttrNode.execute(frame, self, T_ATTR_FN), callArgs, kw);
        }
    }

    @Builtin(name = J_ENCODE, minNumOfPositionalArgs = 2, maxNumOfPositionalArgs = 3)
    protected abstract static class EncodeNode extends PythonTernaryBuiltinNode {
        @Specialization
        Object encode(VirtualFrame frame, PythonObject self, Object input, Object errors,
                        @Cached PyObjectGetAttr getAttrNode,
                        @Cached CodecsEncodeNode encode) {
            return encode.execute(frame, input, getAttrNode.execute(frame, self, T_ATTR_ENCODING), errors);
        }
    }

    @Builtin(name = J_DECODE, minNumOfPositionalArgs = 1, maxNumOfPositionalArgs = 3)
    protected abstract static class CodecDecodeNode extends PythonTernaryBuiltinNode {
        @Specialization
        Object decode(VirtualFrame frame, PythonObject self, Object input, Object errors,
                        @Cached PyObjectGetAttr getAttrNode,
                        @Cached CodecsDecodeNode decode) {
            return decode.execute(frame, input, getAttrNode.execute(frame, self, T_ATTR_ENCODING), errors, true);
        }
    }

    @Builtin(name = J_ENCODE, minNumOfPositionalArgs = 2, maxNumOfPositionalArgs = 3)
    protected abstract static class IncrementalEncodeNode extends PythonTernaryBuiltinNode {
        @Specialization
        Object encode(VirtualFrame frame, PythonObject self, Object input, @SuppressWarnings("unused") Object ffinal,
                        @Cached PyObjectGetAttr getAttrNode,
                        @Cached CodecsEncodeNode encode,
                        @Cached TupleBuiltins.GetItemNode getItemNode) {
            PTuple result = (PTuple) encode.execute(frame, input, getAttrNode.execute(frame, self, T_ATTR_ENCODING), getAttrNode.execute(frame, self, T_ATTR_ERRORS));
            return getItemNode.execute(frame, result, 0);
        }
    }

    @Builtin(name = J_BUFFER_DECODE, minNumOfPositionalArgs = 1, maxNumOfPositionalArgs = 4)
    protected abstract static class IncrementalDecodeNode extends PythonQuaternaryBuiltinNode {
        @Specialization
        Object decode(VirtualFrame frame, PythonObject self, Object input, Object errors, Object ffinal,
                        @Cached PyObjectGetAttr getAttrNode,
                        @Cached CodecsDecodeNode decode) {
            return decode.execute(frame, input, getAttrNode.execute(frame, self, T_ATTR_ENCODING), errors, ffinal);
        }
    }

    @Builtin(name = J_DECODE, minNumOfPositionalArgs = 1, maxNumOfPositionalArgs = 4)
    protected abstract static class StreamDecodeNode extends PythonQuaternaryBuiltinNode {
        @Specialization
        Object decode(VirtualFrame frame, PythonObject self, Object input, Object errors, Object ffinal,
                        @Cached PyObjectGetAttr getAttrNode,
                        @Cached CodecsDecodeNode decode) {
            return decode.execute(frame, input, getAttrNode.execute(frame, self, T_ATTR_ENCODING), errors, ffinal);
        }
    }

    @GenerateUncached
    public abstract static class LookupTextEncoding extends PNodeWithContext {

        public static final TruffleString T_IS_TEXT_ENCODING = tsLiteral("_is_text_encoding");

        public abstract Object execute(Frame frame, TruffleString encoding, TruffleString alternateCommand);

        @Specialization
        Object lookup(VirtualFrame frame, TruffleString encoding, TruffleString alternateCommand,
                        @Cached CodecsModuleBuiltins.InternalLookupNode lookupNode,
                        @Cached PyObjectGetAttr getAttributeNode,
                        @Cached PRaiseNode raiseNode) {
            PTuple codecInfo = lookupNode.execute(frame, encoding);
            Object isTextObj = getAttributeNode.execute(frame, codecInfo, T_IS_TEXT_ENCODING);
            if (!((isTextObj instanceof Boolean) && (boolean) isTextObj)) {
                throw raiseNode.raise(LookupError, IS_NOT_TEXT_ENCODING, encoding, alternateCommand);
            }
            return codecInfo;
        }
    }

    @GenerateUncached
    public abstract static class GetPreferredEncoding extends PNodeWithContext {

        public static final TruffleString T_GETPREFERREDENCODING = tsLiteral("getpreferredencoding");
        public static final TruffleString T_LOCALE = tsLiteral("locale");

        public abstract TruffleString execute(Frame frame);

        @Specialization
        TruffleString getpreferredencoding(VirtualFrame frame,
                        @Cached PyObjectCallMethodObjArgs callMethodNode,
                        @Cached PyObjectStrAsTruffleStringNode strNode) {

            Object locale = AbstractImportNode.importModule(T_LOCALE);
            Object e = callMethodNode.execute(frame, locale, T_GETPREFERREDENCODING);
            return strNode.execute(frame, e);
        }
    }

    @GenerateUncached
    @ImportStatic(PGuards.class)
    public abstract static class MakeIncrementalcodecNode extends PNodeWithContext {

        public abstract Object execute(Frame frame, Object codecInfo, Object errors, TruffleString attrName);

        @Specialization
        static Object getIncEncoder(VirtualFrame frame, Object codecInfo, @SuppressWarnings("unused") PNone errors, TruffleString attrName,
                        @Shared("callMethod") @Cached PyObjectCallMethodObjArgs callMethod) {
            return callMethod.execute(frame, codecInfo, attrName);
        }

        @Specialization(guards = "!isPNone(errors)")
        static Object getIncEncoder(VirtualFrame frame, Object codecInfo, Object errors, TruffleString attrName,
                        @Shared("callMethod") @Cached PyObjectCallMethodObjArgs callMethod) {
            return callMethod.execute(frame, codecInfo, attrName, errors);
        }
    }

    @GenerateUncached
    public abstract static class GetIncrementalEncoderNode extends PNodeWithContext {

        public abstract Object execute(Frame frame, Object codecInfo, TruffleString errors);

        @Specialization
        static Object getIncEncoder(VirtualFrame frame, Object codecInfo, TruffleString errors,
                        @Cached MakeIncrementalcodecNode makeIncrementalcodecNode) {
            return makeIncrementalcodecNode.execute(frame, codecInfo, errors, T_INCREMENTALENCODER);
        }
    }

    @GenerateUncached
    public abstract static class GetIncrementalDecoderNode extends PNodeWithContext {

        public abstract Object execute(Frame frame, Object codecInfo, TruffleString errors);

        @Specialization
        Object getIncEncoder(VirtualFrame frame, Object codecInfo, TruffleString errors,
                        @Cached MakeIncrementalcodecNode makeIncrementalcodecNode) {
            return makeIncrementalcodecNode.execute(frame, codecInfo, errors, T_INCREMENTALDECODER);
        }
    }
}
