/*
 * Copyright 2004-2005 the original author or authors.
 *
 * 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 org.grails.commons;

import org.grails.core.AbstractInjectableGrailsClass;
import groovy.lang.Closure;

import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

import org.grails.encoder.CodecFactory;
import org.grails.encoder.CodecIdentifier;
import org.grails.encoder.CodecMetaClassSupport;
import org.grails.encoder.Decoder;
import org.grails.encoder.DefaultCodecIdentifier;
import org.grails.encoder.Encodeable;
import org.grails.encoder.EncodedAppender;
import org.grails.encoder.Encoder;
import org.grails.encoder.EncodingState;
import org.grails.encoder.EncodingStateRegistry;
import org.grails.encoder.EncodingStateRegistryLookup;
import org.grails.encoder.EncodingStateRegistryLookupHolder;
import org.grails.encoder.StreamingEncoder;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.Ordered;
import org.springframework.util.ReflectionUtils;

/**
 * @author Jeff Brown
 * @since 0.4
 */
public class DefaultGrailsCodecClass extends AbstractInjectableGrailsClass implements InitializingBean, GrailsCodecClass, Ordered {
    public static final String CODEC = CodecArtefactHandler.TYPE;
    private Encoder encoder;
    private Decoder decoder;
    private static int instantionCounter=0;
    private int order = 100 + instantionCounter++;
    private boolean initialized = false;

    public DefaultGrailsCodecClass(Class<?> clazz) {
        super(clazz, CODEC);
    }

    public void afterPropertiesSet() {
        initializeCodec();
    }

    private void initializeCodec() {
        if (initialized) return;
        initialized = true;
        Integer orderSetting = getStaticPropertyValue("order", Integer.class);
        if (orderSetting != null) {
            order = orderSetting;
        }
        Object instance = getReferenceInstance();
        if (Encoder.class.isAssignableFrom(getClazz())) {
            encoder = (Encoder) instance;
            encoder = (Encoder)autowireCodecBean(encoder);
            if (encoder instanceof Ordered) {
                order = ((Ordered)encoder).getOrder();
            }
        }
        if (Decoder.class.isAssignableFrom(getClazz())) {
            decoder = (Decoder) instance;
            decoder = (Decoder)autowireCodecBean(decoder);
            if (decoder instanceof Ordered) {
                order = ((Ordered)decoder).getOrder();
            }
        }
        if (encoder==null && decoder==null) {
            CodecFactory codecFactory=null;
            if (CodecFactory.class.isAssignableFrom(getClazz())) {
                codecFactory=(CodecFactory) instance;
                codecFactory=(CodecFactory)autowireCodecBean(codecFactory);
            }
            if (codecFactory==null) {
                codecFactory=getStaticPropertyValue("codecFactory", CodecFactory.class);
                codecFactory=(CodecFactory)autowireCodecBean(codecFactory);
            }
            if (codecFactory==null) {
                codecFactory=new ClosureCodecFactory(instance);
            }
            encoder=codecFactory.getEncoder();
            decoder=codecFactory.getDecoder();
            if (codecFactory instanceof Ordered) {
                order = ((Ordered)codecFactory).getOrder();
            }
        }
        if (encoder != null) {
            if (encoder instanceof StreamingEncoder) {
                encoder=new StreamingStateAwareEncoderWrapper((StreamingEncoder)encoder);
            } else {
                encoder=new StateAwareEncoderWrapper(encoder);
            }
        }
    }

    protected Object autowireCodecBean(Object existingBean) {
        if (existingBean != null && grailsApplication != null && grailsApplication.getMainContext() != null) {
            AutowireCapableBeanFactory beanFactory = grailsApplication.getMainContext().getAutowireCapableBeanFactory();
            beanFactory.autowireBeanProperties(
                    existingBean, AutowireCapableBeanFactory.AUTOWIRE_BY_NAME, false);
            existingBean = beanFactory.initializeBean(existingBean, "codec");
        }
        return existingBean;
    }

    private class ClosureCodecFactory implements CodecFactory {
        private Encoder encoder;
        private Decoder decoder;
        private final Object codecInstance;

        ClosureCodecFactory(Object codecInstance) {
            this.codecInstance = codecInstance;
            Closure<Object> encoderClosure = getMethodOrClosureMethod(getClazz(), "encode");
            if (encoderClosure != null) {
                encoder=new ClosureEncoder(getName(), encoderClosure);
            }
            Closure<Object> decoderClosure = getMethodOrClosureMethod(getClazz(), "decode");
            if (decoderClosure != null) {
                decoder=new ClosureDecoder(getName(), decoderClosure);
            }
        }

        public Encoder getEncoder() {
            return encoder;
        }

        public Decoder getDecoder() {
            return decoder;
        }

        private Closure<Object> getMethodOrClosureMethod(Class<?> clazz, String methodName) {
            @SuppressWarnings("unchecked")
            Closure<Object> closure = getStaticPropertyValue(methodName, Closure.class);
            if (closure == null) {
                Method method = ReflectionUtils.findMethod(clazz, methodName, (Class<?>[])null);
                if (method != null) {
                    Object owner;
                    if (Modifier.isStatic(method.getModifiers())) {
                        owner=clazz;
                    } else {
                        owner=codecInstance;
                    }
                    return new MethodCallingClosure(owner, method);
                }
                return null;
            } else {
                return closure;
            }
        }
    }

    private static class ClosureDecoder implements Decoder {
        private CodecIdentifier codecIdentifier;
        private Closure<Object> closure;

        public ClosureDecoder(String codecName, Closure<Object> closure) {
            this.codecIdentifier=new DefaultCodecIdentifier(codecName);
            this.closure=closure;
        }

        public CodecIdentifier getCodecIdentifier() {
            return codecIdentifier;
        }

        public Object decode(Object o) {
            return closure.call(o);
        }
    }

    private static class StateAwareEncoderWrapper implements Encoder {
        private Encoder delegate;

        public StateAwareEncoderWrapper(Encoder delegate) {
            this.delegate=delegate;
        }

        public CodecIdentifier getCodecIdentifier() {
            return delegate.getCodecIdentifier();
        }

        public Object encode(Object target) {
            if (target instanceof Encodeable) {
                return ((Encodeable)target).encode(this);
            }

            EncodingStateRegistry encodingState=lookupEncodingState();
            if (encodingState != null && target instanceof CharSequence) {
                if (!encodingState.shouldEncodeWith(this, (CharSequence)target)) {
                    return target;
                }
            }
            Object encoded = delegate.encode(target);
            if (encodingState != null && encoded instanceof CharSequence)
                encodingState.registerEncodedWith(this, (CharSequence)encoded);
            return encoded;
        }

        protected EncodingStateRegistry lookupEncodingState() {
            EncodingStateRegistryLookup encodingStateRegistryLookup = EncodingStateRegistryLookupHolder.getEncodingStateRegistryLookup();
            return encodingStateRegistryLookup != null ? encodingStateRegistryLookup.lookup() : null;
        }

        public void markEncoded(CharSequence string) {
            EncodingStateRegistry encodingState=lookupEncodingState();
            if (encodingState != null) {
                encodingState.registerEncodedWith(this, string);
            }
        }

        public boolean isSafe() {
            return delegate.isSafe();
        }

        public boolean isApplyToSafelyEncoded() {
            return delegate.isApplyToSafelyEncoded();
        }
    }

    private static class StreamingStateAwareEncoderWrapper extends StateAwareEncoderWrapper implements StreamingEncoder {
        private StreamingEncoder delegate;
        public StreamingStateAwareEncoderWrapper(StreamingEncoder delegate) {
            super(delegate);
            this.delegate=delegate;
        }
        public void encodeToStream(Encoder thisInstance, CharSequence source, int offset, int len, EncodedAppender appender,
                EncodingState encodingState) throws IOException {
            delegate.encodeToStream(this, source, offset, len, appender, encodingState);
        }
    }

    private static class ClosureEncoder implements Encoder {
        private CodecIdentifier codecIdentifier;
        private Closure<Object> closure;

        public ClosureEncoder(String codecName, Closure<Object> closure) {
            this.codecIdentifier=new DefaultCodecIdentifier(codecName);
            this.closure=closure;
        }

        public CodecIdentifier getCodecIdentifier() {
            return codecIdentifier;
        }

        public Object encode(Object target) {
            if (target==null) return null;
            return closure.call(target);
        }

        public void markEncoded(CharSequence string) {

        }

        public boolean isSafe() {
            return false;
        }

        public boolean isApplyToSafelyEncoded() {
            return true;
        }
    }

    private static class MethodCallingClosure extends Closure<Object> {
        private static final long serialVersionUID = 1L;
        private Method method;

        public MethodCallingClosure(Object owner, Method method) {
            super(owner);
            maximumNumberOfParameters = 1;
            parameterTypes = new Class[]{Object.class};
            this.method=method;
        }

        protected Object callMethod(Object argument) {
            return ReflectionUtils.invokeMethod(method, !Modifier.isStatic(method.getModifiers()) ? getOwner() : null, argument);
        }

        @Override
        public Object call(Object... args) {
            return doCall(args);
        }

        protected Object doCall(Object[] args) {
            Object target=null;
            if (args != null && args.length > 0)
                target=args[0];
            if (target==null) {
                return null;
            }
            return callMethod(target);
        }
    }

    public Encoder getEncoder() {
        return encoder;
    }

    public Decoder getDecoder() {
        return decoder;
    }

    public void configureCodecMethods() {
        // for compatibility. Not everything (especially unit tests written by existing Grails applications) call afterPropertiesSet(), but everything calls
        // configureCodecMethods() at least once
        initializeCodec();

        new CodecMetaClassSupport().configureCodecMethods(this);
    }

    public int getOrder() {
        return order;
    }
}
