/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you 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.apache.polygene.library.restlet.crud;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
import org.apache.polygene.api.common.Optional;
import org.apache.polygene.api.identity.HasIdentity;
import org.apache.polygene.api.identity.StringIdentity;
import org.apache.polygene.api.injection.scope.Service;
import org.apache.polygene.api.injection.scope.Structure;
import org.apache.polygene.api.injection.scope.This;
import org.apache.polygene.api.mixin.Mixins;
import org.apache.polygene.api.property.Property;
import org.apache.polygene.api.value.ValueBuilder;
import org.apache.polygene.api.value.ValueBuilderFactory;
import org.apache.polygene.library.restlet.FormField;
import org.apache.polygene.library.restlet.RestForm;
import org.apache.polygene.library.restlet.RestLink;
import org.apache.polygene.library.restlet.repository.RepositoryLocator;
import org.apache.polygene.library.restlet.resource.ResourceBuilder;
import org.apache.polygene.library.restlet.resource.ServerResource;
import org.restlet.data.Reference;

@Mixins( EntityResource.Mixin.class )
public interface EntityResource<T extends HasIdentity> extends ServerResource<T>
{
    interface EntityParam
    {
        @Optional
        Property<String> invoke();
    }

    abstract class Mixin<T extends HasIdentity>
        implements EntityResource<T>
    {

        @Structure
        private ValueBuilderFactory vbf;

        @This
        private Parameters<T> parameters;

        @This
        private EntityParam entityParam;

        @Service
        private RepositoryLocator locator;

        @Service
        private ResourceBuilder resourceBuilder;

        @Override
        public T get()
        {
            Class entityType = parameters.entityType().get();
            //noinspection unchecked
            return (T) locator.find( entityType ).get( identity() );
        }

        @Override
        public void put( T value )
        {
            Class<T> entityType = parameters.entityType().get();
            locator.find( entityType ).update( value );
        }

        @Override
        public void delete()
        {
            Class<? extends HasIdentity> entityType = parameters.entityType().get();
            String idOfEntity = parameters.id().get();
            locator.find( entityType ).delete( StringIdentity.identityOf( idOfEntity ) );
        }

        @Override
        public RestLink post( RestForm form )
        {
            Class<T> type = parameters.entityType().get();
            String methodName = entityParam.invoke().get();
            try
            {
                Method method = findMethod( type, methodName );
                if( method == null ) // no arg method doesn't exist, look for single arg method
                {
                    throw new IllegalArgumentException( "Method '" + methodName + "' is not present on " + type.getName() );
                }
                Class entityType = parameters.entityType().get();
                //noinspection unchecked
                T entity = (T) locator.find( entityType ).get( identity() );
                if( method.getParameterCount() == 1 )
                {
                    Class argType = method.getParameterTypes()[ 0 ];
                    Object parameters = createParametersComposite( form, argType );
                    method.invoke( entity, parameters );
                }
                else
                {
                    method.invoke( entity );
                }
            }
            catch( Exception e )
            {
                String message = e.getMessage();
                while( e instanceof InvocationTargetException )
                {
                    e = (Exception) ( (InvocationTargetException) e ).getTargetException();
                    message = e.getMessage();
                }
                throw new RuntimeException( message, e );
            }
            Reference base = parameters.request().get().getResourceRef();
            return resourceBuilder.createRestLink( "", base, org.restlet.data.Method.GET );
        }

        private Object createParametersComposite( RestForm form, Class<?> argType )
        {
            ValueBuilder<?> vb = vbf.newValueBuilderWithState(
                argType,
                descriptor -> {
                    FormField field = form.field( descriptor.qualifiedName().name() );
                    if( field == null )
                    {
                        return null;
                    }
                    Class<?> propertyType = descriptor.valueType().primaryType();
                    Property<String> value = field.value();
                    if( value == null )
                    {
                        return null;
                    }
                    return convertPropertyValue( value.get(), propertyType );
                },
                descriptor -> null,
                descriptor -> null,
                descriptor -> null
            );
            return vb.newInstance();
        }

        private Method findMethod( Class<T> type, String methodName )
        {
            Method[] methods = type.getMethods();
            Method method = null;
            for( Method m : methods )
            {
                if( m.getName().equals( methodName ) )
                {
                    method = m;
                    break;
                }
            }
            return method;
        }

        private Object convertPropertyValue( String input, Class<?> propertyType )
        {
            if( propertyType.equals( String.class ) )
            {
                return input;
            }
            if( Integer.class.isAssignableFrom( propertyType ) )
            {
                return Integer.parseInt( input );
            }
            if( Boolean.class.isAssignableFrom( propertyType ) )
            {
                return Boolean.valueOf( input );
            }
            if( Double.class.isAssignableFrom( propertyType ) )
            {
                return Double.parseDouble( input );
            }
            if( Long.class.isAssignableFrom( propertyType ) )
            {
                return Long.parseLong( input );
            }
            if( Byte.class.isAssignableFrom( propertyType ) )
            {
                return Byte.parseByte( input );
            }
            if( Short.class.isAssignableFrom( propertyType ) )
            {
                return Short.parseShort( input );
            }
            if( Float.class.isAssignableFrom( propertyType ) )
            {
                return Float.parseFloat( input );
            }
            if( Character.class.isAssignableFrom( propertyType ) )
            {
                return input.charAt( 0 );
            }
            if( TemporalAccessor.class.isAssignableFrom( propertyType ) )
            {
                return DateTimeFormatter.ISO_DATE_TIME.parse( input );
            }
            return null;
        }
    }
}
